Skip to content

Commit 65e9160

Browse files
committed
test unicode cases
1 parent a3ef35a commit 65e9160

File tree

2 files changed

+359
-2
lines changed

2 files changed

+359
-2
lines changed
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
import { parse, build } from "..";
2+
3+
describe("Unicode and Edge Cases", () => {
4+
describe("Unicode escape sequences", () => {
5+
it("should handle \\U escape sequences", () => {
6+
const pbxproj = `{
7+
testKey = "\\U0041\\U0042\\U0043";
8+
}`;
9+
const result = parse(pbxproj) as any;
10+
expect(result.testKey).toBe("ABC");
11+
});
12+
13+
it("should handle standard escape sequences", () => {
14+
const pbxproj = `{
15+
newline = "line1\\nline2";
16+
tab = "col1\\tcol2";
17+
quote = "say \\"hello\\"";
18+
backslash = "path\\\\to\\\\file";
19+
}`;
20+
const result = parse(pbxproj) as any;
21+
expect(result.newline).toBe("line1\nline2");
22+
expect(result.tab).toBe("col1\tcol2");
23+
expect(result.quote).toBe('say "hello"');
24+
expect(result.backslash).toBe("path\\to\\file");
25+
});
26+
27+
it("should handle control character escapes", () => {
28+
const pbxproj = `{
29+
bell = "\\a";
30+
backspace = "\\b";
31+
formfeed = "\\f";
32+
carriage = "\\r";
33+
vertical = "\\v";
34+
}`;
35+
const result = parse(pbxproj) as any;
36+
expect(result.bell).toBe("\x07");
37+
expect(result.backspace).toBe("\b");
38+
expect(result.formfeed).toBe("\f");
39+
expect(result.carriage).toBe("\r");
40+
expect(result.vertical).toBe("\v");
41+
});
42+
43+
it("should handle invalid Unicode sequences gracefully", () => {
44+
const pbxproj = `{
45+
invalidUnicode = "\\UZZZZ";
46+
partialUnicode = "\\U123";
47+
}`;
48+
const result = parse(pbxproj) as any;
49+
expect(result.invalidUnicode).toBe("\\UZZZZ");
50+
expect(result.partialUnicode).toBe("\\U123");
51+
});
52+
});
53+
54+
describe("NextStep character mapping", () => {
55+
it("should handle NextStep high-bit characters via octal", () => {
56+
// Test some key NextStep mappings
57+
const pbxproj = `{
58+
nonBreakSpace = "\\200";
59+
copyright = "\\240";
60+
registeredSign = "\\260";
61+
bullet = "\\267";
62+
enDash = "\\261";
63+
emDash = "\\320";
64+
}`;
65+
const result = parse(pbxproj) as any;
66+
expect(result.nonBreakSpace).toBe("\u00a0"); // NO-BREAK SPACE
67+
expect(result.copyright).toBe("\u00a9"); // COPYRIGHT SIGN
68+
expect(result.registeredSign).toBe("\u00ae"); // REGISTERED SIGN
69+
expect(result.bullet).toBe("\u2022"); // BULLET
70+
expect(result.enDash).toBe("\u2013"); // EN DASH
71+
expect(result.emDash).toBe("\u2014"); // EM DASH
72+
});
73+
74+
it("should handle accented characters via NextStep mapping", () => {
75+
const pbxproj = `{
76+
aGrave = "\\201";
77+
aAcute = "\\202";
78+
aTilde = "\\204";
79+
ccedilla = "\\207";
80+
eGrave = "\\210";
81+
oSlash = "\\351";
82+
}`;
83+
const result = parse(pbxproj) as any;
84+
expect(result.aGrave).toBe("\u00c0"); // À
85+
expect(result.aAcute).toBe("\u00c1"); // Á
86+
expect(result.aTilde).toBe("\u00c3"); // Ã
87+
expect(result.ccedilla).toBe("\u00c7"); // Ç
88+
expect(result.eGrave).toBe("\u00c8"); // È
89+
expect(result.oSlash).toBe("\u00d8"); // Ø
90+
});
91+
92+
it("should handle ligatures and special characters", () => {
93+
const pbxproj = `{
94+
fiLigature = "\\256";
95+
flLigature = "\\257";
96+
fractionSlash = "\\244";
97+
fHook = "\\246";
98+
ellipsis = "\\274";
99+
}`;
100+
const result = parse(pbxproj) as any;
101+
expect(result.fiLigature).toBe("\ufb01"); // fi
102+
expect(result.flLigature).toBe("\ufb02"); // fl
103+
expect(result.fractionSlash).toBe("\u2044"); // ⁄
104+
expect(result.fHook).toBe("\u0192"); // ƒ
105+
expect(result.ellipsis).toBe("\u2026"); // …
106+
});
107+
108+
it("should handle replacement characters for undefined mappings", () => {
109+
const pbxproj = `{
110+
notdef1 = "\\376";
111+
notdef2 = "\\377";
112+
}`;
113+
const result = parse(pbxproj) as any;
114+
expect(result.notdef1).toBe("\ufffd"); // REPLACEMENT CHARACTER
115+
expect(result.notdef2).toBe("\ufffd"); // REPLACEMENT CHARACTER
116+
});
117+
});
118+
119+
describe("Octal escape sequences", () => {
120+
it("should handle single digit octal", () => {
121+
const pbxproj = `{
122+
null = "\\0";
123+
one = "\\1";
124+
seven = "\\7";
125+
}`;
126+
const result = parse(pbxproj) as any;
127+
expect(result.null).toBe("\x00");
128+
expect(result.one).toBe("\x01");
129+
expect(result.seven).toBe("\x07");
130+
});
131+
132+
it("should handle two digit octal", () => {
133+
const pbxproj = `{
134+
ten = "\\12";
135+
twentySeven = "\\33";
136+
seventySeven = "\\115";
137+
}`;
138+
const result = parse(pbxproj) as any;
139+
expect(result.ten).toBe("\x0a");
140+
expect(result.twentySeven).toBe("\x1b");
141+
expect(result.seventySeven).toBe("\x4d");
142+
});
143+
144+
it("should handle three digit octal", () => {
145+
const pbxproj = `{
146+
max = "\\377";
147+
middleRange = "\\177";
148+
lowRange = "\\077";
149+
}`;
150+
const result = parse(pbxproj) as any;
151+
expect(result.max).toBe("\ufffd"); // NextStep mapped
152+
expect(result.middleRange).toBe("\x7f");
153+
expect(result.lowRange).toBe("\x3f");
154+
});
155+
156+
it("should handle octal with trailing digits", () => {
157+
const pbxproj = `{
158+
test1 = "\\1234";
159+
test2 = "\\777";
160+
}`;
161+
const result = parse(pbxproj) as any;
162+
// Should parse \123 (octal 123 = decimal 83 = 0x53) and leave "4"
163+
expect(result.test1).toBe("S4");
164+
// \777 (octal 777 = decimal 511) - beyond NextStep range, produces Unicode char 511
165+
expect(result.test2).toBe("ǿ");
166+
});
167+
});
168+
169+
describe("String parsing edge cases", () => {
170+
it("should handle empty strings", () => {
171+
const pbxproj = `{
172+
empty1 = "";
173+
empty2 = '';
174+
}`;
175+
const result = parse(pbxproj) as any;
176+
expect(result.empty1).toBe("");
177+
expect(result.empty2).toBe("");
178+
});
179+
180+
it("should handle mixed quote styles", () => {
181+
const pbxproj = `{
182+
doubleQuoted = "double";
183+
singleQuoted = 'single';
184+
doubleInSingle = 'say "hello"';
185+
singleInDouble = "it's working";
186+
}`;
187+
const result = parse(pbxproj) as any;
188+
expect(result.doubleQuoted).toBe("double");
189+
expect(result.singleQuoted).toBe("single");
190+
expect(result.doubleInSingle).toBe('say "hello"');
191+
expect(result.singleInDouble).toBe("it's working");
192+
});
193+
194+
it("should handle unquoted identifiers", () => {
195+
const pbxproj = `{
196+
unquoted = value;
197+
withNumbers = value123;
198+
withPath = path/to/file;
199+
withDots = com.example.app;
200+
withHyphens = with-hyphens;
201+
withUnderscores = with_underscores;
202+
}`;
203+
const result = parse(pbxproj) as any;
204+
expect(result.unquoted).toBe("value");
205+
expect(result.withNumbers).toBe("value123"); // Mixed alphanumeric stays string
206+
expect(result.withPath).toBe("path/to/file");
207+
expect(result.withDots).toBe("com.example.app");
208+
expect(result.withHyphens).toBe("with-hyphens");
209+
expect(result.withUnderscores).toBe("with_underscores");
210+
});
211+
212+
it("should handle complex nested escapes", () => {
213+
const pbxproj = `{
214+
complex = "prefix\\n\\tindented\\\\backslash\\U0041suffix";
215+
}`;
216+
const result = parse(pbxproj) as any;
217+
expect(result.complex).toBe("prefix\n\tindented\\backslashAsuffix");
218+
});
219+
220+
it("should preserve numeric formatting quirks", () => {
221+
const pbxproj = `{
222+
octalString = 0755;
223+
trailingZero = 1.0;
224+
integer = 42;
225+
float = 3.14;
226+
scientificNotation = 1e5;
227+
}`;
228+
const result = parse(pbxproj) as any;
229+
expect(result.octalString).toBe("0755"); // Preserve octal as string
230+
expect(result.trailingZero).toBe("1.0"); // Preserve trailing zero
231+
expect(result.integer).toBe(42);
232+
expect(result.float).toBe(3.14);
233+
// Scientific notation might not be supported in pbxproj
234+
expect(result.scientificNotation).toBe("1e5");
235+
});
236+
});
237+
238+
describe("Data literal edge cases", () => {
239+
it("should handle minimal data literals", () => {
240+
const pbxproj = `{
241+
singleByte = <48>;
242+
}`;
243+
const result = parse(pbxproj) as any;
244+
expect(result.singleByte).toEqual(Buffer.from("48", 'hex'));
245+
expect(result.singleByte.toString()).toBe("H");
246+
});
247+
248+
it("should handle data with spaces", () => {
249+
const pbxproj = `{
250+
dataWithSpaces = <48 65 6c 6c 6f>;
251+
}`;
252+
const result = parse(pbxproj) as any;
253+
expect(result.dataWithSpaces).toEqual(Buffer.from("48656c6c6f", 'hex'));
254+
expect(result.dataWithSpaces.toString()).toBe("Hello");
255+
});
256+
257+
it("should handle data with newlines", () => {
258+
const pbxproj = `{
259+
multilineData = <48656c6c6f
260+
576f726c64>;
261+
}`;
262+
const result = parse(pbxproj) as any;
263+
expect(result.multilineData).toEqual(Buffer.from("48656c6c6f576f726c64", 'hex'));
264+
expect(result.multilineData.toString()).toBe("HelloWorld");
265+
});
266+
267+
it("should handle uppercase and lowercase hex", () => {
268+
const pbxproj = `{
269+
mixedCase = <48656C6c6F>;
270+
}`;
271+
const result = parse(pbxproj) as any;
272+
expect(result.mixedCase).toEqual(Buffer.from("48656c6c6f", 'hex'));
273+
expect(result.mixedCase.toString()).toBe("Hello");
274+
});
275+
});
276+
277+
describe("Round-trip preservation", () => {
278+
it("should preserve Unicode characters in round-trip", () => {
279+
const original = `{
280+
unicode = "\\U0041\\U00e9\\U2022";
281+
nextStep = "\\240\\267";
282+
mixed = "Hello\\nWorld\\t\\U0041";
283+
}`;
284+
285+
const parsed = parse(original);
286+
const rebuilt = build(parsed);
287+
const reparsed = parse(rebuilt) as any;
288+
289+
expect(reparsed.unicode).toBe("Aé•");
290+
expect(reparsed.nextStep).toBe("©•");
291+
expect(reparsed.mixed).toBe("Hello\nWorld\tA");
292+
});
293+
294+
it("should preserve data literals in round-trip", () => {
295+
const original = `{
296+
data = <48656C6C6F>;
297+
}`;
298+
299+
const parsed = parse(original);
300+
const rebuilt = build(parsed);
301+
const reparsed = parse(rebuilt) as any;
302+
303+
expect(reparsed.data).toEqual(Buffer.from("48656c6c6f", 'hex'));
304+
expect(reparsed.data.toString()).toBe("Hello");
305+
});
306+
307+
it("should preserve numeric formatting in round-trip", () => {
308+
const original = `{
309+
octal = 0755;
310+
trailingZero = 1.0;
311+
integer = 42;
312+
}`;
313+
314+
const parsed = parse(original);
315+
const rebuilt = build(parsed);
316+
317+
// These should be preserved as strings in the output
318+
expect(rebuilt).toContain('0755');
319+
expect(rebuilt).toContain('1.0');
320+
expect(rebuilt).toContain('42');
321+
});
322+
});
323+
324+
describe("Error handling", () => {
325+
it("should handle malformed Unicode gracefully", () => {
326+
const pbxproj = `{
327+
incomplete = "\\U12";
328+
invalid = "\\Ugggg";
329+
}`;
330+
331+
expect(() => parse(pbxproj)).not.toThrow();
332+
const result = parse(pbxproj) as any;
333+
expect(result.incomplete).toBe("\\U12");
334+
expect(result.invalid).toBe("\\Ugggg");
335+
});
336+
337+
it("should handle malformed data literals gracefully", () => {
338+
const pbxproj = `{
339+
oddLength = <48656c6c6f>;
340+
}`;
341+
342+
// Valid hex should parse correctly
343+
expect(() => parse(pbxproj)).not.toThrow();
344+
const result = parse(pbxproj) as any;
345+
expect(result.oddLength).toEqual(Buffer.from("48656c6c6f", 'hex'));
346+
});
347+
348+
it("should handle unclosed strings gracefully", () => {
349+
const pbxproj = `{
350+
unclosed = "missing quote;
351+
}`;
352+
353+
// Parser should handle this error case
354+
expect(() => parse(pbxproj)).toThrow();
355+
});
356+
});
357+
});

src/json/writer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ function ensureQuotes(value: any): string {
6464
return `"${value}"`;
6565
}
6666

67-
// TODO: How to handle buffer? <xx xx xx>
67+
// Format buffer as hex data literal
6868
function formatData(data: Buffer): string {
69-
return `<${data.toString()}>`;
69+
return `<${data.toString('hex').toUpperCase()}>`;
7070
}
7171

7272
function getSortedObjects(objects: Record<string, any>) {

0 commit comments

Comments
 (0)