Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions lib/src/sep/0011/txrep.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,8 @@ class TxRep {
// Preconditions — delegate to generated code.
tx.cond.toTxRep('$prefix.cond', lines);

// Memo — handle manually because the facade uses jsonEncode/jsonDecode
// for legacy format compatibility, while generated code uses
// TxRepHelper.escapeString which emits \xNN hex escapes.
// Memo — handle manually so the decode side can accept both the SEP-0011
// \xNN format and legacy \uNNNN JSON escapes from older SDK versions.
_encodeMemo(tx.memo, lines, '$prefix.memo');

// Operations.
Expand Down Expand Up @@ -238,8 +237,7 @@ class TxRep {
case XdrMemoType.MEMO_NONE:
break;
case XdrMemoType.MEMO_TEXT:
// Use JSON encoding for compatibility with the legacy format.
lines.add('$prefix.text: ${jsonEncode(memo.text)}');
lines.add('$prefix.text: ${TxRepHelper.escapeString(memo.text!)}');
break;
case XdrMemoType.MEMO_ID:
memo.id!.toTxRep('$prefix.id', lines);
Expand Down Expand Up @@ -361,10 +359,33 @@ class TxRep {
} else if (memoTypeStr == 'MEMO_TEXT') {
String? textStr = TxRepHelper.getValue(map, '$prefix.text');
if (textStr == null) throw Exception('missing $prefix.text');
// Decode JSON-encoded string (reverses jsonEncode() in encode).
// Per SEP-0011, MEMO_TEXT uses C-style \xNN UTF-8 byte escapes.
// For backwards compatibility, also accept legacy \uNNNN JSON escapes
// produced by older versions of this SDK.
String text;
if (textStr.startsWith('"') && textStr.endsWith('"')) {
text = jsonDecode(textStr) as String;
if (textStr.startsWith('"') &&
textStr.endsWith('"') &&
textStr.length >= 2) {
// Detect an unescaped \uNNNN sequence: a \u preceded by an even
// number of backslashes (zero or more pairs). An odd count means the
// preceding backslash escapes the \u, which is literal text and
// should be handled by the SEP-0011 unescaper.
final legacyUnicode = RegExp(r'(?<!\\)(?:\\\\)*\\u[0-9a-fA-F]{4}');
if (legacyUnicode.hasMatch(textStr)) {
try {
text = jsonDecode(textStr) as String;
} on FormatException catch (e) {
throw Exception(
'invalid $prefix.text (legacy \\uNNNN form): ${e.message}',
);
}
} else {
try {
text = TxRepHelper.unescapeString(textStr);
} on FormatException catch (e) {
throw Exception('invalid $prefix.text: ${e.message}');
}
}
} else {
text = textStr;
}
Expand Down
23 changes: 19 additions & 4 deletions lib/src/xdr/txrep_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ class TxRepHelper {

/// Escape a string for TxRep double-quoted format.
///
/// Escapes `"`, `\`, represents `\n` as `\\n`, and encodes non-ASCII bytes
/// (outside 0x20–0x7e) as `\\xNN`.
/// Escapes `"`, `\`, represents `\n`, `\r`, `\t` as their C-style short
/// escapes, and encodes any other non-printable or non-ASCII bytes as
/// `\xNN` per UTF-8 byte. Matches the stc reference and the iOS/PHP SDKs.
static String escapeString(String s) {
StringBuffer buf = StringBuffer();
buf.write('"');
Expand All @@ -142,6 +143,12 @@ class TxRepHelper {
} else if (rune == 0x0A) {
// newline
buf.write(r'\n');
} else if (rune == 0x0D) {
// carriage return
buf.write(r'\r');
} else if (rune == 0x09) {
// tab
buf.write(r'\t');
} else if (rune >= 0x20 && rune <= 0x7E) {
buf.writeCharCode(rune);
} else {
Expand All @@ -159,8 +166,8 @@ class TxRepHelper {

/// Unescape a TxRep string value.
///
/// Handles `\"`, `\\`, `\n`, and `\xNN` escape sequences. If the input is
/// enclosed in double quotes, they are stripped.
/// Handles `\"`, `\\`, `\n`, `\r`, `\t`, and `\xNN` escape sequences. If the
/// input is enclosed in double quotes, they are stripped.
static String unescapeString(String s) {
String input = s;
if (input.startsWith('"') && input.endsWith('"') && input.length >= 2) {
Expand Down Expand Up @@ -193,6 +200,14 @@ class TxRepHelper {
flushPendingBytes();
buf.write('\n');
i += 2;
} else if (next == 'r') {
flushPendingBytes();
buf.write('\r');
i += 2;
} else if (next == 't') {
flushPendingBytes();
buf.write('\t');
i += 2;
} else if (next == 'x' && i + 3 < input.length) {
String hexStr = input.substring(i + 2, i + 4);
int? byteVal = int.tryParse(hexStr, radix: 16);
Expand Down
Loading