From a063164fdb54ef59559e4f5fee459decf76583e2 Mon Sep 17 00:00:00 2001 From: Chris Brauchli <952825+cbrauchli@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:12:07 -0400 Subject: [PATCH 1/2] fix(generateUuid): never mint an all-digit id The parser's IntegerValue rule reads an unquoted run of digits that is followed directly by ";" or "," as a number. A value written without a trailing comment, such as the buildConfigurationList or productReference of a target from addTarget(), or the remoteGlobalIDString of a PBXContainerItemProxy from addTargetDependency(), therefore comes back from a write and a parse as a number when its id is all digits. The next write prints it in scientific notation (1.2345678901243459e+23), and the reference names no object. About 1 random id in 60,000 is all digits, so generateUuid() now mints again when it gets one, as it already does on a collision. --- lib/pbxProject.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pbxProject.js b/lib/pbxProject.js index 07fc062..f2f5a21 100644 --- a/lib/pbxProject.js +++ b/lib/pbxProject.js @@ -152,7 +152,9 @@ PBXProject.prototype.generateUuid = function () { .substr(0, 24) .toUpperCase(); - if (this.allUuids().indexOf(id) >= 0) { + // The parser reads an all-digit value with no trailing comment as a number, + // so an id made only of digits would not survive a write and a parse. + if (/^[0-9]+$/.test(id) || this.allUuids().indexOf(id) >= 0) { return this.generateUuid(); } else { return id; From 5faa9a17b0388165c5a10b85fe535eac5d3487e4 Mon Sep 17 00:00:00 2001 From: Chris Brauchli <952825+cbrauchli@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:13:13 -0400 Subject: [PATCH 2/2] test(generateUuid): cover an all-digit random UUID Mocks crypto.randomUUID() to return an all-digit UUID and then a normal one, and checks that generateUuid() returns the second. --- test/pbxProject.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/pbxProject.js b/test/pbxProject.js index 2b62a24..99545d7 100644 --- a/test/pbxProject.js +++ b/test/pbxProject.js @@ -23,6 +23,7 @@ const assert = require('node:assert'); const PBXProject = require('../lib/pbxProject'); const buildConfig = require('./fixtures/buildFiles'); const jsonProject = require('./fixtures/full-project'); +const crypto = require('crypto'); const fs = require('fs'); describe('PBXProject', () => { @@ -143,6 +144,17 @@ describe('PBXProject', () => { assert.ok(uHex.test(newUUID)); }); + it('should not return an id made only of digits', (t) => { + const project = new PBXProject('.'); + const uuids = ['12345678-9012-4345-8678-901234567890', '0a1b2c3d-4e5f-4a6b-8c7d-8e9f0a1b2c3d']; + const randomUUID = t.mock.method(crypto, 'randomUUID', () => uuids.shift()); + + project.hash = buildConfig; + const newUUID = project.generateUuid(); + + assert.equal(newUUID, '0A1B2C3D4E5F4A6B8C7D8E9F'); + assert.equal(randomUUID.mock.callCount(), 2); + }); }); const bcpbx = 'test/parser/projects/build-config.pbxproj';