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+ } ) ;
0 commit comments