Skip to content

Commit c3267a5

Browse files
committed
fix: Fix Cl.parse and Cl.stringify to match JSON.parse and JSON.stringify in escaping characters and string handling
1 parent a3af273 commit c3267a5

File tree

3 files changed

+72
-7
lines changed

3 files changed

+72
-7
lines changed

packages/transactions/src/clarity/parser.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Cl, ClarityValue, TupleCV } from '..';
1+
import { Cl, ClarityValue, ListCV, TupleCV } from '..';
22

33
// COMBINATOR TYPES
44
type Combinator = (str: string) => ParseResult;
@@ -193,7 +193,17 @@ function clBuffer(): Combinator {
193193

194194
/** @ignore helper for string values, removes escaping and unescapes special characters */
195195
function unescape(input: string): string {
196-
return input.replace(/\\\\/g, '\\').replace(/\\(.)/g, '$1');
196+
// To correctly unescape sequences like \n, \t, \", \\, \uXXXX, etc.,
197+
// we can leverage JSON.parse by wrapping the input string in double quotes.
198+
// This ensures that all standard JSON escape sequences are handled according
199+
// to the JSON specification, aligning with the test cases provided.
200+
try {
201+
return JSON.parse(`"${input}"`);
202+
} catch (error) {
203+
throw new Error(
204+
`Failed to unescape string: "${input}" ${error instanceof Error ? error.message : error}`
205+
);
206+
}
197207
}
198208

199209
function clAscii(): Combinator {

packages/transactions/src/clarity/prettyPrint.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99
import { ClarityType, ClarityValue, ListCV, TupleCV } from '.';
1010

11+
function escape(value: string): string {
12+
// Use JSON.stringify to handle all necessary escape sequences (e.g., \n, \r, \t, \", \\, \uXXXX).
13+
// JSON.stringify(value) produces a string like "\"hello\nworld\"", so we slice off the leading and trailing quotes.
14+
return JSON.stringify(value).slice(1, -1);
15+
}
16+
1117
function formatSpace(space: number, depth: number, end = false) {
1218
if (!space) return ' ';
1319
return `\n${' '.repeat(space * (depth - (end ? 1 : 0)))}`;
@@ -80,8 +86,8 @@ function prettyPrintWithDepth(cv: ClarityValue, space = 0, depth: number): strin
8086
if (cv.type === ClarityType.Int) return cv.value.toString();
8187
if (cv.type === ClarityType.UInt) return `u${cv.value.toString()}`;
8288

83-
if (cv.type === ClarityType.StringASCII) return `"${cv.value}"`;
84-
if (cv.type === ClarityType.StringUTF8) return `u"${cv.value}"`;
89+
if (cv.type === ClarityType.StringASCII) return `"${escape(cv.value)}"`;
90+
if (cv.type === ClarityType.StringUTF8) return `u"${escape(cv.value)}"`;
8591

8692
if (cv.type === ClarityType.PrincipalContract) return `'${cv.value}`;
8793
if (cv.type === ClarityType.PrincipalStandard) return `'${cv.value}`;

packages/transactions/tests/clarity.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,8 @@ const TEST_CASES_PARSER = [
792792
{ input: '"hello world"', expected: Cl.stringAscii('hello world') },
793793
{ input: 'u"hello world"', expected: Cl.stringUtf8('hello world') },
794794
{ input: '"hello \\"world\\""', expected: Cl.stringAscii('hello "world"') },
795+
{ input: '"hello \\\\"', expected: Cl.stringAscii('hello \\') },
796+
{ input: '"hello \\\\\\", \\"world\\""', expected: Cl.stringAscii('hello \\", "world"') },
795797
{ input: '(list 1 2 3)', expected: Cl.list([Cl.int(1), Cl.int(2), Cl.int(3)]) },
796798
{ input: '( list 1 2 3 )', expected: Cl.list([Cl.int(1), Cl.int(2), Cl.int(3)]) },
797799
{ input: '( list )', expected: Cl.list([]) },
@@ -830,11 +832,58 @@ const TEST_CASES_PARSER = [
830832
])
831833
),
832834
},
835+
{ input: 'u""', expected: Cl.stringUtf8('') },
836+
{ input: 'u"\\\\"', expected: Cl.stringUtf8('\\') },
837+
{ input: `u"\\n"`, expected: Cl.stringUtf8('\n') },
833838
] as const;
834839

835-
test.each(TEST_CASES_PARSER)('clarity parser %p', ({ input, expected }) => {
836-
const result = parse(input);
837-
expect(result).toEqual(expected);
840+
const TEST_CASES_PARSER_INVERTIBLE = [
841+
{ input: '""', expected: Cl.stringAscii('') },
842+
{ input: '"hello"', expected: Cl.stringAscii('hello') },
843+
{ input: '"\\"hello\\""', expected: Cl.stringAscii('"hello"') },
844+
{ input: '"a\\\\b"', expected: Cl.stringAscii('a\\b') },
845+
{ input: 'u"a\\\\\\\\b"', expected: Cl.stringUtf8('a\\\\b') },
846+
{ input: 'u"a\\"b"', expected: Cl.stringUtf8('a"b') },
847+
{ input: 'u"こんにちは"', expected: Cl.stringUtf8('こんにちは') },
848+
849+
{ input: '"\\\\path\\\\to\\\\file"', expected: Cl.stringAscii('\\path\\to\\file') },
850+
{ input: '"\\b\\f\\n\\r\\t\\"\\\\"', expected: Cl.stringAscii('\b\f\n\r\t"\\') },
851+
852+
{
853+
input: '"Line1\\nLine2\\u0002 \\"Quote\\" \\\\ slash"',
854+
expected: Cl.stringAscii('Line1\nLine2\u0002 "Quote" \\ slash'),
855+
},
856+
857+
{ input: 'u"résumé – ångström"', expected: Cl.stringUtf8('résumé – ångström') },
858+
] as const;
859+
860+
test.each([...TEST_CASES_PARSER, ...TEST_CASES_PARSER_INVERTIBLE])(
861+
'clarity parser %p',
862+
({ input, expected }) => {
863+
const result = parse(input);
864+
expect(result).toEqual(expected);
865+
}
866+
);
867+
868+
test.each(TEST_CASES_PARSER_INVERTIBLE)('clarity parser inverseable %p', ({ input, expected }) => {
869+
const parsed = parse(input);
870+
expect(parsed).toEqual(expected);
871+
872+
const stringified = Cl.stringify(parsed);
873+
expect(stringified).toEqual(input);
838874
});
839875

876+
test.each(TEST_CASES_PARSER_INVERTIBLE)(
877+
'clarity parser string handling matches JSON JS implementation %p',
878+
({ input, expected }) => {
879+
if (expected.type !== 'utf8' && expected.type !== 'ascii') return; // Only strings
880+
if (expected.type === 'utf8') input = input.replace('u"', '"') as any; // Remove the u" prefix for UTF-8 strings
881+
882+
// String handling in Cl.parse/Cl.stringify should match the JSON.parse/JSON.stringify implementation
883+
const parsed = JSON.parse(input);
884+
const stringified = JSON.stringify(parsed);
885+
expect(stringified).toEqual(input);
886+
}
887+
);
888+
840889
// const TEST_CASES_PARSER_THROW = []; // todo: e.g. `{}`

0 commit comments

Comments
 (0)