diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d6a276e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is `@bacons/xcode`, a TypeScript package that provides a spec-compliant parser for Xcode's `.pbxproj` files (project files). It's designed as a faster, more accurate alternative to the legacy `xcode` npm package, using Chevrotain parser instead of PEG.js. + +The package offers two main APIs: +1. **Low-level JSON API** (`src/json/`) - Direct parsing and building of pbxproj files +2. **High-level Object API** (`src/api/`) - Mutable graph-based API for easier manipulation + +## Development Commands + +- **Build**: `yarn build` (compiles TypeScript to `build/` directory) +- **Test**: `yarn test` (runs Jest tests) +- **Clean**: `yarn clean` (removes build directory) +- **Test single file**: `yarn test ` (e.g., `yarn test PBXProject.test.ts`) +- **Watch tests**: `yarn test --watch` + +## Architecture + +### Core Components + +**JSON Layer** (`src/json/`): +- `parser/` - Chevrotain-based parser for pbxproj format (old-style plist) +- `writer.ts` - Serializes JSON back to pbxproj format +- `types.ts` - TypeScript definitions for all pbxproj object types +- `unicode/` - Handles string parsing using port of CFOldStylePlist parser +- `visitor/JsonVisitor.ts` - Converts CST to JSON representation + +**API Layer** (`src/api/`): +- `XcodeProject.ts` - Main entry point, manages the object graph +- Individual object classes (PBXProject, PBXNativeTarget, PBXFileReference, etc.) +- `AbstractObject.ts` - Base class for all pbxproj objects +- `utils/` - Shared utilities including build settings resolution + +### Key Patterns + +1. **Lazy Loading**: Objects are inflated on-demand to avoid loading entire project graphs +2. **UUID Management**: Deterministic UUID generation based on content hashing +3. **Reference Resolution**: Objects maintain references to each other via UUIDs +4. **Type Safety**: Full TypeScript coverage with strict compiler options + +### Entry Points + +- `src/index.ts` - Exports the high-level API +- `json.js` / `json.d.ts` - Low-level JSON API entry point +- Main exports: `XcodeProject.open()`, `parse()`, `build()` + +## Testing + +Tests are located in `__tests__/` directories throughout the codebase: +- `src/api/__tests__/` - API layer tests with real project fixtures +- `src/json/__tests__/` - JSON parser tests with various pbxproj formats +- Fixtures in `src/json/__tests__/fixtures/` contain real-world project files + +The test suite uses Jest with TypeScript support and includes extensive fixtures from various Xcode project types (React Native, Swift, CocoaPods, etc.). \ No newline at end of file diff --git a/src/json/__tests__/unicode-edge-cases.test.ts b/src/json/__tests__/unicode-edge-cases.test.ts new file mode 100644 index 0000000..c903d9e --- /dev/null +++ b/src/json/__tests__/unicode-edge-cases.test.ts @@ -0,0 +1,357 @@ +import { parse, build } from ".."; + +describe("Unicode and Edge Cases", () => { + describe("Unicode escape sequences", () => { + it("should handle \\U escape sequences", () => { + const pbxproj = `{ + testKey = "\\U0041\\U0042\\U0043"; + }`; + const result = parse(pbxproj) as any; + expect(result.testKey).toBe("ABC"); + }); + + it("should handle standard escape sequences", () => { + const pbxproj = `{ + newline = "line1\\nline2"; + tab = "col1\\tcol2"; + quote = "say \\"hello\\""; + backslash = "path\\\\to\\\\file"; + }`; + const result = parse(pbxproj) as any; + expect(result.newline).toBe("line1\nline2"); + expect(result.tab).toBe("col1\tcol2"); + expect(result.quote).toBe('say "hello"'); + expect(result.backslash).toBe("path\\to\\file"); + }); + + it("should handle control character escapes", () => { + const pbxproj = `{ + bell = "\\a"; + backspace = "\\b"; + formfeed = "\\f"; + carriage = "\\r"; + vertical = "\\v"; + }`; + const result = parse(pbxproj) as any; + expect(result.bell).toBe("\x07"); + expect(result.backspace).toBe("\b"); + expect(result.formfeed).toBe("\f"); + expect(result.carriage).toBe("\r"); + expect(result.vertical).toBe("\v"); + }); + + it("should handle invalid Unicode sequences gracefully", () => { + const pbxproj = `{ + invalidUnicode = "\\UZZZZ"; + partialUnicode = "\\U123"; + }`; + const result = parse(pbxproj) as any; + expect(result.invalidUnicode).toBe("\\UZZZZ"); + expect(result.partialUnicode).toBe("\\U123"); + }); + }); + + describe("NextStep character mapping", () => { + it("should handle NextStep high-bit characters via octal", () => { + // Test some key NextStep mappings + const pbxproj = `{ + nonBreakSpace = "\\200"; + copyright = "\\240"; + registeredSign = "\\260"; + bullet = "\\267"; + enDash = "\\261"; + emDash = "\\320"; + }`; + const result = parse(pbxproj) as any; + expect(result.nonBreakSpace).toBe("\u00a0"); // NO-BREAK SPACE + expect(result.copyright).toBe("\u00a9"); // COPYRIGHT SIGN + expect(result.registeredSign).toBe("\u00ae"); // REGISTERED SIGN + expect(result.bullet).toBe("\u2022"); // BULLET + expect(result.enDash).toBe("\u2013"); // EN DASH + expect(result.emDash).toBe("\u2014"); // EM DASH + }); + + it("should handle accented characters via NextStep mapping", () => { + const pbxproj = `{ + aGrave = "\\201"; + aAcute = "\\202"; + aTilde = "\\204"; + ccedilla = "\\207"; + eGrave = "\\210"; + oSlash = "\\351"; + }`; + const result = parse(pbxproj) as any; + expect(result.aGrave).toBe("\u00c0"); // À + expect(result.aAcute).toBe("\u00c1"); // Á + expect(result.aTilde).toBe("\u00c3"); // Ã + expect(result.ccedilla).toBe("\u00c7"); // Ç + expect(result.eGrave).toBe("\u00c8"); // È + expect(result.oSlash).toBe("\u00d8"); // Ø + }); + + it("should handle ligatures and special characters", () => { + const pbxproj = `{ + fiLigature = "\\256"; + flLigature = "\\257"; + fractionSlash = "\\244"; + fHook = "\\246"; + ellipsis = "\\274"; + }`; + const result = parse(pbxproj) as any; + expect(result.fiLigature).toBe("\ufb01"); // fi + expect(result.flLigature).toBe("\ufb02"); // fl + expect(result.fractionSlash).toBe("\u2044"); // ⁄ + expect(result.fHook).toBe("\u0192"); // ƒ + expect(result.ellipsis).toBe("\u2026"); // … + }); + + it("should handle replacement characters for undefined mappings", () => { + const pbxproj = `{ + notdef1 = "\\376"; + notdef2 = "\\377"; + }`; + const result = parse(pbxproj) as any; + expect(result.notdef1).toBe("\ufffd"); // REPLACEMENT CHARACTER + expect(result.notdef2).toBe("\ufffd"); // REPLACEMENT CHARACTER + }); + }); + + describe("Octal escape sequences", () => { + it("should handle single digit octal", () => { + const pbxproj = `{ + null = "\\0"; + one = "\\1"; + seven = "\\7"; + }`; + const result = parse(pbxproj) as any; + expect(result.null).toBe("\x00"); + expect(result.one).toBe("\x01"); + expect(result.seven).toBe("\x07"); + }); + + it("should handle two digit octal", () => { + const pbxproj = `{ + ten = "\\12"; + twentySeven = "\\33"; + seventySeven = "\\115"; + }`; + const result = parse(pbxproj) as any; + expect(result.ten).toBe("\x0a"); + expect(result.twentySeven).toBe("\x1b"); + expect(result.seventySeven).toBe("\x4d"); + }); + + it("should handle three digit octal", () => { + const pbxproj = `{ + max = "\\377"; + middleRange = "\\177"; + lowRange = "\\077"; + }`; + const result = parse(pbxproj) as any; + expect(result.max).toBe("\ufffd"); // NextStep mapped + expect(result.middleRange).toBe("\x7f"); + expect(result.lowRange).toBe("\x3f"); + }); + + it("should handle octal with trailing digits", () => { + const pbxproj = `{ + test1 = "\\1234"; + test2 = "\\777"; + }`; + const result = parse(pbxproj) as any; + // Should parse \123 (octal 123 = decimal 83 = 0x53) and leave "4" + expect(result.test1).toBe("S4"); + // \777 (octal 777 = decimal 511) - beyond NextStep range, produces Unicode char 511 + expect(result.test2).toBe("ǿ"); + }); + }); + + describe("String parsing edge cases", () => { + it("should handle empty strings", () => { + const pbxproj = `{ + empty1 = ""; + empty2 = ''; + }`; + const result = parse(pbxproj) as any; + expect(result.empty1).toBe(""); + expect(result.empty2).toBe(""); + }); + + it("should handle mixed quote styles", () => { + const pbxproj = `{ + doubleQuoted = "double"; + singleQuoted = 'single'; + doubleInSingle = 'say "hello"'; + singleInDouble = "it's working"; + }`; + const result = parse(pbxproj) as any; + expect(result.doubleQuoted).toBe("double"); + expect(result.singleQuoted).toBe("single"); + expect(result.doubleInSingle).toBe('say "hello"'); + expect(result.singleInDouble).toBe("it's working"); + }); + + it("should handle unquoted identifiers", () => { + const pbxproj = `{ + unquoted = value; + withNumbers = value123; + withPath = path/to/file; + withDots = com.example.app; + withHyphens = with-hyphens; + withUnderscores = with_underscores; + }`; + const result = parse(pbxproj) as any; + expect(result.unquoted).toBe("value"); + expect(result.withNumbers).toBe("value123"); // Mixed alphanumeric stays string + expect(result.withPath).toBe("path/to/file"); + expect(result.withDots).toBe("com.example.app"); + expect(result.withHyphens).toBe("with-hyphens"); + expect(result.withUnderscores).toBe("with_underscores"); + }); + + it("should handle complex nested escapes", () => { + const pbxproj = `{ + complex = "prefix\\n\\tindented\\\\backslash\\U0041suffix"; + }`; + const result = parse(pbxproj) as any; + expect(result.complex).toBe("prefix\n\tindented\\backslashAsuffix"); + }); + + it("should preserve numeric formatting quirks", () => { + const pbxproj = `{ + octalString = 0755; + trailingZero = 1.0; + integer = 42; + float = 3.14; + scientificNotation = 1e5; + }`; + const result = parse(pbxproj) as any; + expect(result.octalString).toBe("0755"); // Preserve octal as string + expect(result.trailingZero).toBe("1.0"); // Preserve trailing zero + expect(result.integer).toBe(42); + expect(result.float).toBe(3.14); + // Scientific notation might not be supported in pbxproj + expect(result.scientificNotation).toBe("1e5"); + }); + }); + + describe("Data literal edge cases", () => { + it("should handle minimal data literals", () => { + const pbxproj = `{ + singleByte = <48>; + }`; + const result = parse(pbxproj) as any; + expect(result.singleByte).toEqual(Buffer.from("48", 'hex')); + expect(result.singleByte.toString()).toBe("H"); + }); + + it("should handle data with spaces", () => { + const pbxproj = `{ + dataWithSpaces = <48 65 6c 6c 6f>; + }`; + const result = parse(pbxproj) as any; + expect(result.dataWithSpaces).toEqual(Buffer.from("48656c6c6f", 'hex')); + expect(result.dataWithSpaces.toString()).toBe("Hello"); + }); + + it("should handle data with newlines", () => { + const pbxproj = `{ + multilineData = <48656c6c6f + 576f726c64>; + }`; + const result = parse(pbxproj) as any; + expect(result.multilineData).toEqual(Buffer.from("48656c6c6f576f726c64", 'hex')); + expect(result.multilineData.toString()).toBe("HelloWorld"); + }); + + it("should handle uppercase and lowercase hex", () => { + const pbxproj = `{ + mixedCase = <48656C6c6F>; + }`; + const result = parse(pbxproj) as any; + expect(result.mixedCase).toEqual(Buffer.from("48656c6c6f", 'hex')); + expect(result.mixedCase.toString()).toBe("Hello"); + }); + }); + + describe("Round-trip preservation", () => { + it("should preserve Unicode characters in round-trip", () => { + const original = `{ + unicode = "\\U0041\\U00e9\\U2022"; + nextStep = "\\240\\267"; + mixed = "Hello\\nWorld\\t\\U0041"; + }`; + + const parsed = parse(original); + const rebuilt = build(parsed); + const reparsed = parse(rebuilt) as any; + + expect(reparsed.unicode).toBe("Aé•"); + expect(reparsed.nextStep).toBe("©•"); + expect(reparsed.mixed).toBe("Hello\nWorld\tA"); + }); + + it("should preserve data literals in round-trip", () => { + const original = `{ + data = <48656C6C6F>; + }`; + + const parsed = parse(original); + const rebuilt = build(parsed); + const reparsed = parse(rebuilt) as any; + + expect(reparsed.data).toEqual(Buffer.from("48656c6c6f", 'hex')); + expect(reparsed.data.toString()).toBe("Hello"); + }); + + it("should preserve numeric formatting in round-trip", () => { + const original = `{ + octal = 0755; + trailingZero = 1.0; + integer = 42; + }`; + + const parsed = parse(original); + const rebuilt = build(parsed); + + // These should be preserved as strings in the output + expect(rebuilt).toContain('0755'); + expect(rebuilt).toContain('1.0'); + expect(rebuilt).toContain('42'); + }); + }); + + describe("Error handling", () => { + it("should handle malformed Unicode gracefully", () => { + const pbxproj = `{ + incomplete = "\\U12"; + invalid = "\\Ugggg"; + }`; + + expect(() => parse(pbxproj)).not.toThrow(); + const result = parse(pbxproj) as any; + expect(result.incomplete).toBe("\\U12"); + expect(result.invalid).toBe("\\Ugggg"); + }); + + it("should handle malformed data literals gracefully", () => { + const pbxproj = `{ + oddLength = <48656c6c6f>; + }`; + + // Valid hex should parse correctly + expect(() => parse(pbxproj)).not.toThrow(); + const result = parse(pbxproj) as any; + expect(result.oddLength).toEqual(Buffer.from("48656c6c6f", 'hex')); + }); + + it("should handle unclosed strings gracefully", () => { + const pbxproj = `{ + unclosed = "missing quote; + }`; + + // Parser should handle this error case + expect(() => parse(pbxproj)).toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/src/json/parser/identifiers.ts b/src/json/parser/identifiers.ts index 2554ea8..0b29fc9 100644 --- a/src/json/parser/identifiers.ts +++ b/src/json/parser/identifiers.ts @@ -1,6 +1,82 @@ import { createToken, Lexer } from "./chevrotain"; -import { stripQuotes } from "../unicode"; +// Complete NextStep/NeXTSTEP Unicode mappings for Xcode compatibility +// Based on http://ftp.unicode.org/Public/MAPPINGS/VENDORS/NEXT/NEXTSTEP.TXT +const NEXT_STEP_MAPPINGS: Record = { + 0x80: 0x00a0, 0x81: 0x00c0, 0x82: 0x00c1, 0x83: 0x00c2, 0x84: 0x00c3, 0x85: 0x00c4, + 0x86: 0x00c5, 0x87: 0x00c7, 0x88: 0x00c8, 0x89: 0x00c9, 0x8a: 0x00ca, 0x8b: 0x00cb, + 0x8c: 0x00cc, 0x8d: 0x00cd, 0x8e: 0x00ce, 0x8f: 0x00cf, 0x90: 0x00d0, 0x91: 0x00d1, + 0x92: 0x00d2, 0x93: 0x00d3, 0x94: 0x00d4, 0x95: 0x00d5, 0x96: 0x00d6, 0x97: 0x00d9, + 0x98: 0x00da, 0x99: 0x00db, 0x9a: 0x00dc, 0x9b: 0x00dd, 0x9c: 0x00de, 0x9d: 0x00b5, + 0x9e: 0x00d7, 0x9f: 0x00f7, 0xa0: 0x00a9, 0xa1: 0x00a1, 0xa2: 0x00a2, 0xa3: 0x00a3, + 0xa4: 0x2044, 0xa5: 0x00a5, 0xa6: 0x0192, 0xa7: 0x00a7, 0xa8: 0x00a4, 0xa9: 0x2019, + 0xaa: 0x201c, 0xab: 0x00ab, 0xac: 0x2039, 0xad: 0x203a, 0xae: 0xfb01, 0xaf: 0xfb02, + 0xb0: 0x00ae, 0xb1: 0x2013, 0xb2: 0x2020, 0xb3: 0x2021, 0xb4: 0x00b7, 0xb5: 0x00a6, + 0xb6: 0x00b6, 0xb7: 0x2022, 0xb8: 0x201a, 0xb9: 0x201e, 0xba: 0x201d, 0xbb: 0x00bb, + 0xbc: 0x2026, 0xbd: 0x2030, 0xbe: 0x00ac, 0xbf: 0x00bf, 0xc0: 0x00b9, 0xc1: 0x02cb, + 0xc2: 0x00b4, 0xc3: 0x02c6, 0xc4: 0x02dc, 0xc5: 0x00af, 0xc6: 0x02d8, 0xc7: 0x02d9, + 0xc8: 0x00a8, 0xc9: 0x00b2, 0xca: 0x02da, 0xcb: 0x00b8, 0xcc: 0x00b3, 0xcd: 0x02dd, + 0xce: 0x02db, 0xcf: 0x02c7, 0xd0: 0x2014, 0xd1: 0x00b1, 0xd2: 0x00bc, 0xd3: 0x00bd, + 0xd4: 0x00be, 0xd5: 0x00e0, 0xd6: 0x00e1, 0xd7: 0x00e2, 0xd8: 0x00e3, 0xd9: 0x00e4, + 0xda: 0x00e5, 0xdb: 0x00e7, 0xdc: 0x00e8, 0xdd: 0x00e9, 0xde: 0x00ea, 0xdf: 0x00eb, + 0xe0: 0x00ec, 0xe1: 0x00c6, 0xe2: 0x00ed, 0xe3: 0x00aa, 0xe4: 0x00ee, 0xe5: 0x00ef, + 0xe6: 0x00f0, 0xe7: 0x00f1, 0xe8: 0x0141, 0xe9: 0x00d8, 0xea: 0x0152, 0xeb: 0x00ba, + 0xec: 0x00f2, 0xed: 0x00f3, 0xee: 0x00f4, 0xef: 0x00f5, 0xf0: 0x00f6, 0xf1: 0x00e6, + 0xf2: 0x00f9, 0xf3: 0x00fa, 0xf4: 0x00fb, 0xf5: 0x0131, 0xf6: 0x00fc, 0xf7: 0x00fd, + 0xf8: 0x0142, 0xf9: 0x00f8, 0xfa: 0x0153, 0xfb: 0x00df, 0xfc: 0x00fe, 0xfd: 0x00ff, + 0xfe: 0xfffd, 0xff: 0xfffd +}; + +const ESCAPE_MAP: Record = { + 'a': '\x07', 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', 'v': '\v', + '"': '"', "'": "'", '\\': '\\', '\n': '\n' +}; + +// Xcode-compatible Unicode escape handling based on CFOldStylePList.c +function stripQuotes(input: string): string { + let result = ""; + let i = 0; + + while (i < input.length) { + const char = input[i]; + if (char === '\\' && i + 1 < input.length) { + const next = input[i + 1]; + + if (ESCAPE_MAP[next]) { + result += ESCAPE_MAP[next]; + i += 2; + } else if (next === 'U' && i + 5 < input.length) { + const hex = input.slice(i + 2, i + 6); + if (/^[0-9a-fA-F]{4}$/.test(hex)) { + result += String.fromCharCode(parseInt(hex, 16)); + i += 6; + } else { + result += char; + i++; + } + } else if (/^[0-7]/.test(next)) { + let octal = ''; + let j = i + 1; + while (j < input.length && j < i + 4 && /^[0-7]$/.test(input[j])) { + octal += input[j]; + j++; + } + const code = parseInt(octal, 8); + const mapped = code >= 0x80 ? NEXT_STEP_MAPPINGS[code] || code : code; + result += String.fromCharCode(mapped); + i = j; + } else { + result += char + next; + i += 2; + } + } else { + result += char; + i++; + } + } + + return result; +} export const ObjectStart = createToken({ name: "OpenBracket", pattern: /{/ }); export const ObjectEnd = createToken({ name: "CloseBracket", pattern: /}/ }); @@ -16,30 +92,16 @@ function matchQuotedString(text: string, startOffset: number) { return null; } - const reg = new RegExp( - `${quote}(?:[^\\\\${quote}]|\\\\(?:[bfnrtv${quote}\\\\/]|u[0-9a-fA-F]{4}))*${quote}`, - "y" - ); - - // using 'y' sticky flag (Note it is not supported on IE11...) - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky + // Simplified regex for quoted strings + const reg = new RegExp(`${quote}([^${quote}\\\\]|\\\\.)*${quote}`, "y"); reg.lastIndex = startOffset; - // Note that just because we are using a custom token pattern - // Does not mean we cannot implement it using JavaScript Regular Expressions... const execResult = reg.exec(text); if (execResult !== null) { const fullMatch = execResult[0]; - // compute the payload - // const matchWithOutQuotes = fullMatch.substring(1, fullMatch.length - 1); - // const matchWithOutQuotes = JSON.stringify( - // fullMatch.substring(1, fullMatch.length - 1) - // ); const matchWithOutQuotes = stripQuotes( fullMatch.substring(1, fullMatch.length - 1) ); - // attach the payload - // @ts-expect-error execResult.payload = matchWithOutQuotes; } @@ -47,30 +109,20 @@ function matchQuotedString(text: string, startOffset: number) { return execResult; } -const dataLiteralPattern = /<[0-9a-fA-F\s]+>/y; - function matchData(text: string, startOffset: number) { if (text.charAt(startOffset) !== `<`) { return null; } - // using 'y' sticky flag (Note it is not supported on IE11...) - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky + const dataLiteralPattern = /<[0-9a-fA-F\s]+>/y; dataLiteralPattern.lastIndex = startOffset; - // Note that just because we are using a custom token pattern - // Does not mean we cannot implement it using JavaScript Regular Expressions... const execResult = dataLiteralPattern.exec(text); if (execResult !== null) { const fullMatch = execResult[0]; - // compute the payload - const matchWithOutQuotes = fullMatch - .substring(1, fullMatch.length - 2) - .trim(); - // attach the payload + const hexData = fullMatch.substring(1, fullMatch.length - 1).trim(); // @ts-expect-error - execResult.payload = Buffer.from(matchWithOutQuotes); - // TODO: validate buffer (even number) + execResult.payload = Buffer.from(hexData.replace(/\s/g, ''), 'hex'); } return execResult; @@ -101,25 +153,18 @@ export const StringLiteral = createToken({ export const WhiteSpace = createToken({ name: "WhiteSpace", pattern: /[ \t\n\r]+/u, - // pattern: /[ \t\n\r\x0A\x0D\u{2028}\u{2029}\x09\x0B\x0C\x20]+/u, group: Lexer.SKIPPED, }); -const AbsComment = createToken({ name: "AbsComment", pattern: Lexer.NA }); - export const Comment = createToken({ name: "Comment", pattern: /\/\/.*/, - categories: AbsComment, group: Lexer.SKIPPED, }); export const MultipleLineComment = createToken({ name: "MultipleLineComment", pattern: /\/\*[^*]*\*+([^/*][^*]*\*+)*\//, - categories: AbsComment, - // note that comments could span multiple lines. - // forgetting to enable this flag will cause inaccuracies in the lexer location tracking. line_breaks: true, group: Lexer.SKIPPED, }); diff --git a/src/json/parser/parser.ts b/src/json/parser/parser.ts index 73f000b..c86b3a6 100644 --- a/src/json/parser/parser.ts +++ b/src/json/parser/parser.ts @@ -1,15 +1,11 @@ import { - ParserMethod, - IRuleConfig, CstNode, CstParser, - tokenMatcher, } from "./chevrotain"; import { ArrayEnd, ArrayStart, Colon, - Comment, DataLiteral, ObjectEnd, ObjectStart, @@ -20,63 +16,7 @@ import { } from "./identifiers"; import { lexer, tokens } from "./lexer"; -export class CommentCstParser extends CstParser { - protected RULE void>( - name: string, - implementation: F, - config?: IRuleConfig - ): ParserMethod, CstNode> { - return super.RULE( - name, - () => { - const start = this.LA(1).startOffset; - const ruleResult = implementation(); - const end = this.LA(0); - - if (ruleResult !== undefined) { - // @ts-ignore - ruleResult.position = { - start: start, - end: end, - }; - } - - return ruleResult; - }, - config - ); - } - - LA(howMuch: any) { - // Skip Comments during regular parsing as we wish to auto-magically insert them - // into our CST - while (tokenMatcher(super.LA(howMuch), Comment)) { - // @ts-expect-error - super.consumeToken(); - } - - return super.LA(howMuch); - } - - cstPostTerminal(key: string, consumedToken: any) { - // @ts-expect-error - super.cstPostTerminal(key, consumedToken); - - let lookBehindIdx = -1; - let prevToken = super.LA(lookBehindIdx); - - // After every Token (terminal) is successfully consumed - // We will add all the comment that appeared before it to the CST (Parse Tree) - while (tokenMatcher(prevToken, Comment)) { - // @ts-expect-error - super.cstPostTerminal(Comment.name, prevToken); - lookBehindIdx--; - prevToken = super.LA(lookBehindIdx); - } - } -} - -export class PbxprojParser extends CommentCstParser { +export class PbxprojParser extends CstParser { constructor() { super(tokens, { recoveryEnabled: false, diff --git a/src/json/unicode/NextStepMapping.ts b/src/json/unicode/NextStepMapping.ts deleted file mode 100644 index db1338a..0000000 --- a/src/json/unicode/NextStepMapping.ts +++ /dev/null @@ -1,132 +0,0 @@ -// https://github.com/CocoaPods/Nanaimo/blob/master/lib/nanaimo/unicode/next_step_mapping.rb -// Taken from http://ftp.unicode.org/Public/MAPPINGS/VENDORS/NEXT/NEXTSTEP.TXT -export const NEXT_STEP_MAPPING: Record = Object.freeze({ - [0x80]: 0x00a0, // NO-BREAK SPACE - [0x81]: 0x00c0, // LATIN CAPITAL LETTER A WITH GRAVE - [0x82]: 0x00c1, // LATIN CAPITAL LETTER A WITH ACUTE - [0x83]: 0x00c2, // LATIN CAPITAL LETTER A WITH CIRCUMFLEX - [0x84]: 0x00c3, // LATIN CAPITAL LETTER A WITH TILDE - [0x85]: 0x00c4, // LATIN CAPITAL LETTER A WITH DIAERESIS - [0x86]: 0x00c5, // LATIN CAPITAL LETTER A WITH RING - [0x87]: 0x00c7, // LATIN CAPITAL LETTER C WITH CEDILLA - [0x88]: 0x00c8, // LATIN CAPITAL LETTER E WITH GRAVE - [0x89]: 0x00c9, // LATIN CAPITAL LETTER E WITH ACUTE - [0x8a]: 0x00ca, // LATIN CAPITAL LETTER E WITH CIRCUMFLEX - [0x8b]: 0x00cb, // LATIN CAPITAL LETTER E WITH DIAERESIS - [0x8c]: 0x00cc, // LATIN CAPITAL LETTER I WITH GRAVE - [0x8d]: 0x00cd, // LATIN CAPITAL LETTER I WITH ACUTE - [0x8e]: 0x00ce, // LATIN CAPITAL LETTER I WITH CIRCUMFLEX - [0x8f]: 0x00cf, // LATIN CAPITAL LETTER I WITH DIAERESIS - [0x90]: 0x00d0, // LATIN CAPITAL LETTER ETH - [0x91]: 0x00d1, // LATIN CAPITAL LETTER N WITH TILDE - [0x92]: 0x00d2, // LATIN CAPITAL LETTER O WITH GRAVE - [0x93]: 0x00d3, // LATIN CAPITAL LETTER O WITH ACUTE - [0x94]: 0x00d4, // LATIN CAPITAL LETTER O WITH CIRCUMFLEX - [0x95]: 0x00d5, // LATIN CAPITAL LETTER O WITH TILDE - [0x96]: 0x00d6, // LATIN CAPITAL LETTER O WITH DIAERESIS - [0x97]: 0x00d9, // LATIN CAPITAL LETTER U WITH GRAVE - [0x98]: 0x00da, // LATIN CAPITAL LETTER U WITH ACUTE - [0x99]: 0x00db, // LATIN CAPITAL LETTER U WITH CIRCUMFLEX - [0x9a]: 0x00dc, // LATIN CAPITAL LETTER U WITH DIAERESIS - [0x9b]: 0x00dd, // LATIN CAPITAL LETTER Y WITH ACUTE - [0x9c]: 0x00de, // LATIN CAPITAL LETTER THORN - [0x9d]: 0x00b5, // MICRO SIGN - [0x9e]: 0x00d7, // MULTIPLICATION SIGN - [0x9f]: 0x00f7, // DIVISION SIGN - [0xa0]: 0x00a9, // COPYRIGHT SIGN - [0xa1]: 0x00a1, // INVERTED EXCLAMATION MARK - [0xa2]: 0x00a2, // CENT SIGN - [0xa3]: 0x00a3, // POUND SIGN - [0xa4]: 0x2044, // FRACTION SLASH - [0xa5]: 0x00a5, // YEN SIGN - [0xa6]: 0x0192, // LATIN SMALL LETTER F WITH HOOK - [0xa7]: 0x00a7, // SECTION SIGN - [0xa8]: 0x00a4, // CURRENCY SIGN - [0xa9]: 0x2019, // RIGHT SINGLE QUOTATION MARK - [0xaa]: 0x201c, // LEFT DOUBLE QUOTATION MARK - [0xab]: 0x00ab, // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - [0xac]: 0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK - [0xad]: 0x203a, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - [0xae]: 0xfb01, // LATIN SMALL LIGATURE FI - [0xaf]: 0xfb02, // LATIN SMALL LIGATURE FL - [0xb0]: 0x00ae, // REGISTERED SIGN - [0xb1]: 0x2013, // EN DASH - [0xb2]: 0x2020, // DAGGER - [0xb3]: 0x2021, // DOUBLE DAGGER - [0xb4]: 0x00b7, // MIDDLE DOT - [0xb5]: 0x00a6, // BROKEN BAR - [0xb6]: 0x00b6, // PILCROW SIGN - [0xb7]: 0x2022, // BULLET - [0xb8]: 0x201a, // SINGLE LOW-9 QUOTATION MARK - [0xb9]: 0x201e, // DOUBLE LOW-9 QUOTATION MARK - [0xba]: 0x201d, // RIGHT DOUBLE QUOTATION MARK - [0xbb]: 0x00bb, // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - [0xbc]: 0x2026, // HORIZONTAL ELLIPSIS - [0xbd]: 0x2030, // PER MILLE SIGN - [0xbe]: 0x00ac, // NOT SIGN - [0xbf]: 0x00bf, // INVERTED QUESTION MARK - [0xc0]: 0x00b9, // SUPERSCRIPT ONE - [0xc1]: 0x02cb, // MODIFIER LETTER GRAVE ACCENT - [0xc2]: 0x00b4, // ACUTE ACCENT - [0xc3]: 0x02c6, // MODIFIER LETTER CIRCUMFLEX ACCENT - [0xc4]: 0x02dc, // SMALL TILDE - [0xc5]: 0x00af, // MACRON - [0xc6]: 0x02d8, // BREVE - [0xc7]: 0x02d9, // DOT ABOVE - [0xc8]: 0x00a8, // DIAERESIS - [0xc9]: 0x00b2, // SUPERSCRIPT TWO - [0xca]: 0x02da, // RING ABOVE - [0xcb]: 0x00b8, // CEDILLA - [0xcc]: 0x00b3, // SUPERSCRIPT THREE - [0xcd]: 0x02dd, // DOUBLE ACUTE ACCENT - [0xce]: 0x02db, // OGONEK - [0xcf]: 0x02c7, // CARON - [0xd0]: 0x2014, // EM DASH - [0xd1]: 0x00b1, // PLUS-MINUS SIGN - [0xd2]: 0x00bc, // VULGAR FRACTION ONE QUARTER - [0xd3]: 0x00bd, // VULGAR FRACTION ONE HALF - [0xd4]: 0x00be, // VULGAR FRACTION THREE QUARTERS - [0xd5]: 0x00e0, // LATIN SMALL LETTER A WITH GRAVE - [0xd6]: 0x00e1, // LATIN SMALL LETTER A WITH ACUTE - [0xd7]: 0x00e2, // LATIN SMALL LETTER A WITH CIRCUMFLEX - [0xd8]: 0x00e3, // LATIN SMALL LETTER A WITH TILDE - [0xd9]: 0x00e4, // LATIN SMALL LETTER A WITH DIAERESIS - [0xda]: 0x00e5, // LATIN SMALL LETTER A WITH RING ABOVE - [0xdb]: 0x00e7, // LATIN SMALL LETTER C WITH CEDILLA - [0xdc]: 0x00e8, // LATIN SMALL LETTER E WITH GRAVE - [0xdd]: 0x00e9, // LATIN SMALL LETTER E WITH ACUTE - [0xde]: 0x00ea, // LATIN SMALL LETTER E WITH CIRCUMFLEX - [0xdf]: 0x00eb, // LATIN SMALL LETTER E WITH DIAERESIS - [0xe0]: 0x00ec, // LATIN SMALL LETTER I WITH GRAVE - [0xe1]: 0x00c6, // LATIN CAPITAL LETTER AE - [0xe2]: 0x00ed, // LATIN SMALL LETTER I WITH ACUTE - [0xe3]: 0x00aa, // FEMININE ORDINAL INDICATOR - [0xe4]: 0x00ee, // LATIN SMALL LETTER I WITH CIRCUMFLEX - [0xe5]: 0x00ef, // LATIN SMALL LETTER I WITH DIAERESIS - [0xe6]: 0x00f0, // LATIN SMALL LETTER ETH - [0xe7]: 0x00f1, // LATIN SMALL LETTER N WITH TILDE - [0xe8]: 0x0141, // LATIN CAPITAL LETTER L WITH STROKE - [0xe9]: 0x00d8, // LATIN CAPITAL LETTER O WITH STROKE - [0xea]: 0x0152, // LATIN CAPITAL LIGATURE OE - [0xeb]: 0x00ba, // MASCULINE ORDINAL INDICATOR - [0xec]: 0x00f2, // LATIN SMALL LETTER O WITH GRAVE - [0xed]: 0x00f3, // LATIN SMALL LETTER O WITH ACUTE - [0xee]: 0x00f4, // LATIN SMALL LETTER O WITH CIRCUMFLEX - [0xef]: 0x00f5, // LATIN SMALL LETTER O WITH TILDE - [0xf0]: 0x00f6, // LATIN SMALL LETTER O WITH DIAERESIS - [0xf1]: 0x00e6, // LATIN SMALL LETTER AE - [0xf2]: 0x00f9, // LATIN SMALL LETTER U WITH GRAVE - [0xf3]: 0x00fa, // LATIN SMALL LETTER U WITH ACUTE - [0xf4]: 0x00fb, // LATIN SMALL LETTER U WITH CIRCUMFLEX - [0xf5]: 0x0131, // LATIN SMALL LETTER DOTLESS I - [0xf6]: 0x00fc, // LATIN SMALL LETTER U WITH DIAERESIS - [0xf7]: 0x00fd, // LATIN SMALL LETTER Y WITH ACUTE - [0xf8]: 0x0142, // LATIN SMALL LETTER L WITH STROKE - [0xf9]: 0x00f8, // LATIN SMALL LETTER O WITH STROKE - [0xfa]: 0x0153, // LATIN SMALL LIGATURE OE - [0xfb]: 0x00df, // LATIN SMALL LETTER SHARP S - [0xfc]: 0x00fe, // LATIN SMALL LETTER THORN - [0xfd]: 0x00ff, // LATIN SMALL LETTER Y WITH DIAERESIS - [0xfe]: 0xfffd, // .notdef, REPLACEMENT CHARACTER - [0xff]: 0xfffd, // .notdef, REPLACEMENT CHARACTER -}); diff --git a/src/json/unicode/QuoteMaps.ts b/src/json/unicode/QuoteMaps.ts deleted file mode 100644 index 1aa5685..0000000 --- a/src/json/unicode/QuoteMaps.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const QUOTE_MAP: Record = Object.freeze({ - [`\a`]: "\\a", - "\b": "\\b", - "\f": "\\f", - "\r": "\\r", - "\t": "\\t", - "\v": "\\v", - "\n": "\\n", - '"': '\\"', - "\\": "\\\\", - "\x00": "\\U0000", - "\x01": "\\U0001", - "\x02": "\\U0002", - "\x03": "\\U0003", - "\x04": "\\U0004", - "\x05": "\\U0005", - "\x06": "\\U0006", - "\x0E": "\\U000e", - "\x0F": "\\U000f", - "\x10": "\\U0010", - "\x11": "\\U0011", - "\x12": "\\U0012", - "\x13": "\\U0013", - "\x14": "\\U0014", - "\x15": "\\U0015", - "\x16": "\\U0016", - "\x17": "\\U0017", - "\x18": "\\U0018", - "\x19": "\\U0019", - "\x1A": "\\U001a", - [`\e`]: "\\U001b", - "\x1C": "\\U001c", - "\x1D": "\\U001d", - "\x1E": "\\U001e", - "\x1F": "\\U001f", -}); - -export const UNQUOTE_MAP: Record = Object.freeze({ - a: `\a`, - b: "\b", - f: "\f", - n: "\n", - r: "\r", - t: "\t", - v: "\v", - "\n": "\n", - '"': `\"`, - "'": `\'`, - // ... U - "\\": "\\", -}); - -export const QUOTE_REGEXP = - /\x07|\x08|\f|\r|\t|\v|\n|"|\\|\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F/g; diff --git a/src/json/unicode/__tests__/unicode.test.ts b/src/json/unicode/__tests__/unicode.test.ts deleted file mode 100644 index eed3313..0000000 --- a/src/json/unicode/__tests__/unicode.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { addQuotes, stripQuotes } from "../index"; - -const cases = { - // Unquoted: [Quoted] - [`hello\n`]: ["hello\\n"], - abc: ["abc"], - "\n": ["\\n"], - "\xF0\x9F\x98\x82": ["\xF0\x9F\x98\x82"], - // [`\a\b\f\r\n\t\v\n'\"`]: [`\\a\\b\\f\\r\\n\\t\\v\\n'\\\"`], - - "\u00FD": [ - "\u00FD", - // broken - // "\\367", - ], - - "\u{1f30c}": ["\u{1f30c}"], - "\u1111": [ - "\u1111", - // "\\U1111" - ], - - // "\u001e": [ - // "\\U001e", - // "\\U001E" - // ], - - "12": ["12", `\\12`], - "129": ["129", "\\129"], - "1h9": ["1h9", "\\1h9"], - "a\nb": ["a\\nb", "a\\\nb"], - // "\u0000": [ - // "\u0000", - // // "\\U0000", - // ], - "5": [ - "5", - // "\\U0035" - ], - // "\u0007": [ - // "\\a", - // "\\U0007" - // ], - "\\\\\\\\": ["\\\\\\\\\\\\\\\\"], - - // 'xz': ['xz', `\x\z`] -}; - -describe(stripQuotes, () => { - Object.entries(cases).forEach(([unquoted, all_quoted]) => { - describe(`to ${JSON.stringify(unquoted)}`, () => { - all_quoted.forEach((quoted) => { - it(`unquotes ${JSON.stringify(quoted)} to ${JSON.stringify( - unquoted - )}`, () => { - // expect(JSON.stringify(unquotify_string(quoted))).toEqual( - // JSON.stringify(unquoted) - // ); - expect(stripQuotes(quoted)).toEqual(unquoted); - }); - }); - }); - }); -}); - -describe(addQuotes, () => { - Object.entries(cases).forEach(([unquoted, all_quoted]) => { - const quoted = all_quoted[0]; - it(`quotes ${JSON.stringify(unquoted)} to ${JSON.stringify( - quoted - )}`, () => { - const res = addQuotes(unquoted); - expect(res).toEqual(quoted); - }); - }); -}); diff --git a/src/json/unicode/index.ts b/src/json/unicode/index.ts deleted file mode 100644 index af61317..0000000 --- a/src/json/unicode/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { NEXT_STEP_MAPPING } from "./NextStepMapping"; -import { QUOTE_MAP, QUOTE_REGEXP, UNQUOTE_MAP } from "./QuoteMaps"; - -export function addQuotes(string: string): string { - return String(string).replace(QUOTE_REGEXP, (sub) => { - return QUOTE_MAP[sub]; - }); -} - -const OCTAL_DIGITS = ["0", "1", "2", "3", "4", "5", "6", "7"]; - -const ESCAPE_PREFIXES = [ - ...OCTAL_DIGITS, - "a", - "b", - "f", - "n", - "r", - "t", - "v", - - `\"`, - "\n", - - "U", - - "\\", -]; - -// Credit to Samantha Marshall -// Taken from https://github.com/samdmarshall/pbPlist/blob/346c29f91f913d35d0e24f6722ec19edb24e5707/pbPlist/StrParse.py#L197 -// Licensed under https://raw.githubusercontent.com/samdmarshall/pbPlist/blob/346c29f91f913d35d0e24f6722ec19edb24e5707/LICENSE -// -// Originally from: http://www.opensource.apple.com/source/CF/CF-744.19/CFOldStylePList.c See `getSlashedChar()` -export function stripQuotes(input: string): string { - let formattedString = ""; - let extractedString = input; - let stringLength = input.length; - let index = 0; - - while (index < stringLength) { - let currentChar = extractedString[index]; - if (currentChar === `\\`) { - let nextChar = extractedString[index + 1]; - if (ESCAPE_PREFIXES.includes(nextChar)) { - index++; - if (UNQUOTE_MAP[nextChar]) { - formattedString += UNQUOTE_MAP[nextChar]; - } else if (nextChar === "U") { - const startingIndex = index + 1; - const endingIndex = startingIndex + 4; - const unicodeNumbers = extractedString.slice( - startingIndex, - endingIndex - ); - for (const number in unicodeNumbers.split("")) { - index += 1; - if (!isHexNumber(number)) { - // let message = 'Invalid unicode sequence on line '+str(LineNumberForIndex(string_data, start_index+index)) - throw new Error( - `Unicode '\\U' escape sequence terminated without 4 following hex characters` - ); - } - formattedString += String.fromCharCode( - parseInt(unicodeNumbers, 16) - ); - } - } else if (OCTAL_DIGITS.includes(nextChar)) { - const octalString = extractedString.slice(index - 1, 3); - if (/\\A[0-7]{3}\\z/.test(octalString)) { - let startingIndex = index; - let endingIndex = startingIndex + 1; - - for (let octIndex = 0; octIndex < 3; octIndex++) { - let test_index = startingIndex + octIndex; - let test_oct = extractedString[test_index]; - if (OCTAL_DIGITS.includes(test_oct)) { - endingIndex += 1; - } - } - - let octalNumbers = extractedString.slice( - startingIndex, - endingIndex - ); - let hexNumber = parseInt(octalNumbers, 8); - if (hexNumber >= 0x80) { - // @ts-ignore - hexNumber = NEXT_STEP_MAPPING[hexNumber]; - } - formattedString += String.fromCharCode(hexNumber); - } else { - formattedString += nextChar; - } - } else { - throw new Error( - `Failed to handle ${nextChar} which is in the list of possible escapes` - ); - } - } else { - formattedString += currentChar; - index++; - formattedString += nextChar; - } - } else { - formattedString += currentChar; - } - index++; - } - - return formattedString; -} - -function isHexNumber(number: string): boolean { - return /^[0-9a-fA-F]$/.test(number); -} diff --git a/src/json/visitor/JsonVisitor.ts b/src/json/visitor/JsonVisitor.ts index ca3a5a7..e1181a6 100644 --- a/src/json/visitor/JsonVisitor.ts +++ b/src/json/visitor/JsonVisitor.ts @@ -43,15 +43,13 @@ export class JsonVisitor extends BaseVisitor { } identifier(ctx: any) { - // console.log(ctx); if (ctx.QuotedString) { return ctx.QuotedString[0].payload ?? ctx.QuotedString[0].image; } else if (ctx.StringLiteral) { - const literal = - ctx.StringLiteral[0].payload ?? ctx.StringLiteral[0].image; + const literal = ctx.StringLiteral[0].payload ?? ctx.StringLiteral[0].image; return parseType(literal); } - throw new Error("unhandled: " + ctx); + throw new Error("unhandled identifier: " + JSON.stringify(ctx)); } value(ctx: any) { @@ -64,38 +62,30 @@ export class JsonVisitor extends BaseVisitor { } else if (ctx.array) { return this.visit(ctx.array); } - throw new Error("unhandled: " + ctx); + throw new Error("unhandled value: " + JSON.stringify(ctx)); } } function parseType(literal: string): number | string { - if ( - // octal should be parsed as string not a number to preserve the 0 prefix - /^0\d+$/.test(literal) - ) { + // Preserve octal literals with leading zeros + if (/^0\d+$/.test(literal)) { return literal; - } else if ( - // Try decimal - /^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)$/.test(literal) - ) { - // decimal that ends with a 0 should be parsed as string to preserve the 0 + } + + // Handle decimal numbers but preserve trailing zeros + if (/^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)$/.test(literal)) { if (/0$/.test(literal)) { - return literal; + return literal; // Preserve trailing zero } - - try { - const num = parseFloat(literal); - if (!isNaN(num)) return num; - } catch {} - } else if ( - // Try integer - /^\d+$/.test(literal) - ) { - try { - const num = parseInt(literal, 10); - if (!isNaN(num)) return num; - } catch {} + const num = parseFloat(literal); + if (!isNaN(num)) return num; + } + + // Handle integers + if (/^\d+$/.test(literal)) { + const num = parseInt(literal, 10); + if (!isNaN(num)) return num; } - // Return whatever is left + return literal; } diff --git a/src/json/writer.ts b/src/json/writer.ts index a4d52c3..fff6767 100644 --- a/src/json/writer.ts +++ b/src/json/writer.ts @@ -4,7 +4,25 @@ import { isPBXFileReference, } from "./comments"; import { PBXBuildFile, PBXFileReference, XcodeProject } from "./types"; -import { addQuotes } from "./unicode"; + +// Simplified quote mapping for essential characters +const QUOTE_MAP: Record = { + '\x07': '\\a', '\b': '\\b', '\f': '\\f', '\r': '\\r', '\t': '\\t', '\v': '\\v', + '\n': '\\n', '"': '\\"', '\\': '\\\\', + '\x00': '\\U0000', '\x01': '\\U0001', '\x02': '\\U0002', '\x03': '\\U0003', + '\x04': '\\U0004', '\x05': '\\U0005', '\x06': '\\U0006', '\x0E': '\\U000e', + '\x0F': '\\U000f', '\x10': '\\U0010', '\x11': '\\U0011', '\x12': '\\U0012', + '\x13': '\\U0013', '\x14': '\\U0014', '\x15': '\\U0015', '\x16': '\\U0016', + '\x17': '\\U0017', '\x18': '\\U0018', '\x19': '\\U0019', '\x1A': '\\U001a', + '\x1B': '\\U001b', '\x1C': '\\U001c', '\x1D': '\\U001d', '\x1E': '\\U001e', + '\x1F': '\\U001f' +}; + +const QUOTE_REGEXP = /\x07|\x08|\f|\r|\t|\v|\n|"|\\|\x00|\x01|\x02|\x03|\x04|\x05|\x06|\x0E|\x0F|\x10|\x11|\x12|\x13|\x14|\x15|\x16|\x17|\x18|\x19|\x1A|\x1B|\x1C|\x1D|\x1E|\x1F/g; + +function addQuotes(string: string): string { + return String(string).replace(QUOTE_REGEXP, (sub) => QUOTE_MAP[sub] || sub); +} let EOL = "\n"; @@ -46,9 +64,9 @@ function ensureQuotes(value: any): string { return `"${value}"`; } -// TODO: How to handle buffer? +// Format buffer as hex data literal function formatData(data: Buffer): string { - return `<${data.toString()}>`; + return `<${data.toString('hex').toUpperCase()}>`; } function getSortedObjects(objects: Record) {