From 1c57eed275a03c5fc9a7ed41e21eb8082757df8c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:05:22 +0800 Subject: [PATCH] fix(apple): retain binary UID identity through archive projection --- docs/native-investigation.md | 5 +- .../apple/InterfaceBuilderAnalysis.ts | 9 +- .../apple/KeyedArchiveReader.test.ts | 23 +++++ src/artifacts/apple/KeyedArchiveReader.ts | 15 ++-- src/artifacts/apple/PlistScalars.ts | 52 +++++++++++ .../apple/PropertyListDecodeBudget.ts | 86 ++++++++++++++++++- .../apple/interfaceBuilderKeyedArchive.ts | 16 +--- src/domain/apple/keyedArchive.ts | 12 +-- src/domain/apple/plistUid.ts | 32 +++++++ src/domain/apple/plistValue.ts | 5 +- .../filesystem/interfaceBuilderBundle.test.ts | 19 ++++ tests/fixtures/keyedArchiveUidShapes.ts | 45 ++++++++++ 12 files changed, 285 insertions(+), 34 deletions(-) create mode 100644 src/domain/apple/plistUid.ts create mode 100644 tests/fixtures/keyedArchiveUidShapes.ts diff --git a/docs/native-investigation.md b/docs/native-investigation.md index b2792a5958..b582ddf4fa 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -474,7 +474,10 @@ complete observation. shared/cyclic references, class descriptors, raw serialized fields and malformed/unresolved/nil references. Missing fields stay absent; unknown classes are preserved. Select `root`, `offset` and `limit` explicitly. - Object `kind` describes the decoded archive value: data, dates and numbers + Binary references retain their serialized type-8 UID identity; ordinary + dictionaries with `UID` or `CF$UID` keys remain authored data. XML has no UID + scalar, so its dictionary UID convention remains ambiguous with authored + dictionaries of the same shape. Object `kind` describes the decoded archive value: data, dates and numbers remain `scalar` when their JSON value uses a `$plist_type` wrapper. An actual dictionary with the same fields remains `dictionary`. XML must be well-formed. Data literals use diff --git a/src/artifacts/apple/InterfaceBuilderAnalysis.ts b/src/artifacts/apple/InterfaceBuilderAnalysis.ts index 6e683011d8..bcd5d0a00f 100644 --- a/src/artifacts/apple/InterfaceBuilderAnalysis.ts +++ b/src/artifacts/apple/InterfaceBuilderAnalysis.ts @@ -52,6 +52,7 @@ import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js" import { decodeXmlPlistText } from "../../domain/propertyListXmlText.js"; import { decodeBinaryPlistScalars, + retainBinaryPlistUidIdentity, scanBinaryPlistScalars, UnsupportedBinaryPlistUidError, type BinaryPlistScalarDecodePlan, @@ -184,6 +185,7 @@ export const analyzeInterfaceBuilderBundle = async (input: { xmlText, { numberProjection: true, + uidProjection: true, budget: "the aggregate Interface Builder decode budget", fail: interfaceBuilderDecodeBudgetFailure, }, @@ -925,8 +927,11 @@ const decodePlist = ( : undefined; const { value, omittedPrototypeKeys, numberLiterals } = binary ? { - value: parseBinary( - decodeBinaryPlistScalars(bytes, binaryScalarMetadata!, scalarPlan), + value: retainBinaryPlistUidIdentity( + parseBinary( + decodeBinaryPlistScalars(bytes, binaryScalarMetadata!, scalarPlan), + ), + scalarPlan, ), omittedPrototypeKeys: binaryPrototypeKeyObserved ? null : 0, numberLiterals: binaryScalarMetadata!.literals, diff --git a/src/artifacts/apple/KeyedArchiveReader.test.ts b/src/artifacts/apple/KeyedArchiveReader.test.ts index 8c574344c1..864307ce39 100644 --- a/src/artifacts/apple/KeyedArchiveReader.test.ts +++ b/src/artifacts/apple/KeyedArchiveReader.test.ts @@ -1,3 +1,4 @@ +import { binaryKeyedArchiveUidShapes } from "../../../tests/fixtures/keyedArchiveUidShapes.js"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -814,3 +815,25 @@ describe("XML plist nesting preflight", () => { ).toThrow(new RangeError(plistNestingLimitMessage())); }); }); + +it("distinguishes binary UID records from authored UID-shaped dictionaries", () => { + const graph = decodeKeyedArchiveBytes(binaryKeyedArchiveUidShapes(), { + offset: 0, + limit: 10, + }); + expect(graph.objects[1]?.value).toMatchObject({ + ordinary: { UID: 2 }, + xmlConvention: { CF$UID: 2 }, + actual: { UID: 2 }, + }); + expect( + graph.references + .filter(({ source }) => source === 1) + .map(({ path, target }) => ({ path, target })), + ).toEqual([ + { path: ["$class"], target: 3 }, + { path: ["actual"], target: 2 }, + { path: ["shared", 0], target: 2 }, + { path: ["shared", 1], target: 2 }, + ]); +}); diff --git a/src/artifacts/apple/KeyedArchiveReader.ts b/src/artifacts/apple/KeyedArchiveReader.ts index ac3e28d811..850a5edae4 100644 --- a/src/artifacts/apple/KeyedArchiveReader.ts +++ b/src/artifacts/apple/KeyedArchiveReader.ts @@ -17,6 +17,7 @@ import { import { createPlistNumberProjection } from "../../domain/apple/plistNumbers.js"; import { decodeBinaryPlistScalars, + retainBinaryPlistUidIdentity, scanBinaryPlistScalars, UnsupportedBinaryPlistUidError, } from "./PlistScalars.js"; @@ -77,6 +78,7 @@ export const decodeKeyedArchiveBytes = ( : new RangeError(message), bounds: "decoder", numberProjection: true, + uidProjection: true, }) : undefined; const binaryScalarMetadata = binary @@ -85,12 +87,15 @@ export const decodeKeyedArchiveBytes = ( const parsed = binaryExpansion !== undefined ? { - value: parseBinary( - decodeBinaryPlistScalars( - bytes, - binaryScalarMetadata!, - binaryExpansion!, + value: retainBinaryPlistUidIdentity( + parseBinary( + decodeBinaryPlistScalars( + bytes, + binaryScalarMetadata!, + binaryExpansion!, + ), ), + binaryExpansion, ), numberLiterals: binaryScalarMetadata!.literals, omittedPrototypeKeys: binaryExpansion.binaryPrototypeKeyObserved diff --git a/src/artifacts/apple/PlistScalars.ts b/src/artifacts/apple/PlistScalars.ts index 17afdd4aca..60d7cb29c9 100644 --- a/src/artifacts/apple/PlistScalars.ts +++ b/src/artifacts/apple/PlistScalars.ts @@ -1,3 +1,4 @@ +import { markBinaryPlistDictionary } from "../../domain/apple/plistUid.js"; import { createPlistNumberCollector, type PlistNumberLiterals, @@ -32,7 +33,15 @@ const containsSortedOffset = ( return false; }; +/** A bounded linked value path retained by the existing binary reference walk. */ +export interface BinaryPlistUidPath { + readonly parent: BinaryPlistUidPath | null; + readonly key: string | number; +} + export interface BinaryPlistScalarDecodePlan { + /** Present only when an archive reader requested serialized UID identity. */ + readonly binaryUidPaths?: readonly (BinaryPlistUidPath | null)[]; readonly reachableDateOffsets: readonly number[]; readonly overlappingDateOffsets: readonly number[]; readonly reachableWideIntegerOffsets: readonly number[]; @@ -382,6 +391,49 @@ export const scanBinaryPlistScalars = ( return result(incomplete); }; +/** Restore UID identity from validated source locations, never from dictionary shape. */ +export const retainBinaryPlistUidIdentity = ( + value: unknown, + plan: BinaryPlistScalarDecodePlan, +): unknown => { + if (plan.binaryUidPaths === undefined) return value; + const pending: unknown[] = [value]; + while (pending.length > 0) { + const item = pending.pop(); + if (Array.isArray(item)) { + for (const child of item) pending.push(child); + } else if ( + item !== null && + typeof item === "object" && + !(item instanceof Date) && + !(item instanceof Uint8Array) + ) { + markBinaryPlistDictionary(item); + for (const child of Object.values(item)) pending.push(child); + } + } + for (let path of plan.binaryUidPaths) { + const keys: (string | number)[] = []; + while (path !== null) { + keys.push(path.key); + path = path.parent; + } + let item: unknown = value; + for (let index = keys.length - 1; index >= 0; index -= 1) { + if (item === null || typeof item !== "object") { + item = undefined; + break; + } + item = Object.getOwnPropertyDescriptor(item, keys[index]!)?.value; + } + if (item !== null && typeof item === "object") { + const uid = Object.getOwnPropertyDescriptor(item, "UID")?.value; + if (typeof uid === "number") markBinaryPlistDictionary(item, uid); + } + } + return value; +}; + /** Preserve source scalars through plist@5's lossy Number and Date parser. */ export const decodeBinaryPlistScalars = ( bytes: Buffer, diff --git a/src/artifacts/apple/PropertyListDecodeBudget.ts b/src/artifacts/apple/PropertyListDecodeBudget.ts index 5251c651a4..9452e98405 100644 --- a/src/artifacts/apple/PropertyListDecodeBudget.ts +++ b/src/artifacts/apple/PropertyListDecodeBudget.ts @@ -5,6 +5,7 @@ import { binaryDateRequiresExactSeconds, binaryPlistObjectEnd, binaryPlistUidNormalizationWidth, + type BinaryPlistUidPath, } from "./PlistScalars.js"; /** Preflight the representation created by the pinned plist decoder. */ @@ -14,17 +15,19 @@ export const preflightPropertyListDecode = ( xmlText?: string, options: Pick< BinaryPlistExpansionOptions, - "numberProjection" | "budget" | "fail" + "numberProjection" | "uidProjection" | "budget" | "fail" > = {}, ): PlistDecodePreflight => { const { numberProjection = false, + uidProjection = false, budget = "the property list decode budget", fail = propertyListDecodeBudgetFailure, } = options; const expansion = hasBinaryPlistSignature(bytes) ? estimateBinaryPlistExpansion(bytes, maxBytes, { numberProjection, + uidProjection, budget, fail, }) @@ -154,6 +157,8 @@ const propertyListDecodeBudgetFailure = ( export interface BinaryPlistExpansionOptions { /** Include original number metadata and exact-integer attribute projection. */ readonly numberProjection?: boolean; + /** Retain typed UID locations for Foundation archive interpretation. */ + readonly uidProjection?: boolean; /** Budget named in failure messages. */ readonly budget?: string; /** Error for budget, depth, and reference-cycle failures. */ @@ -168,6 +173,7 @@ export interface BinaryPlistExpansionOptions { /** Preflight size and binary-key observations; XML keys are inspected by decode. */ export interface PlistDecodePreflight { + readonly binaryUidPaths?: readonly (BinaryPlistUidPath | null)[]; readonly estimatedBytes: number; readonly binaryPrototypeKeyObserved: boolean; /** Number of expanded references to precision-unknown overlapping dates. */ @@ -240,6 +246,7 @@ export const estimateBinaryPlistExpansion = ( fail = propertyListDecodeBudgetFailure, bounds = "strict", numberProjection = false, + uidProjection = false, }: BinaryPlistExpansionOptions = {}, ): PlistDecodePreflight => { if (bytes.length < 40) @@ -290,8 +297,8 @@ export const estimateBinaryPlistExpansion = ( throw new TypeError("binary plist object reference count is invalid"); // Reserve source metadata and reachable-offset indexes for scalar projection. // Without projection, the traversal retains only structural validation state. - const objectBytes = numberProjection ? 320 : 16; - const expandedNodeBytes = numberProjection ? 224 : 96; + const objectBytes = numberProjection || uidProjection ? 320 : 16; + const expandedNodeBytes = numberProjection || uidProjection ? 224 : 96; // Scalar projection may require a same-sized parser-compatible input copy // for wide integers and sub-millisecond dates. Reserve before allocation. let estimate = @@ -306,7 +313,50 @@ export const estimateBinaryPlistExpansion = ( depth: number; exit: boolean; dictionaryKey: boolean; - }> = [{ object: topObject, depth: 0, exit: false, dictionaryKey: false }]; + path?: BinaryPlistUidPath | null; + }> = [ + { + object: topObject, + depth: 0, + exit: false, + dictionaryKey: false, + ...(uidProjection ? { path: null } : {}), + }, + ]; + const binaryUidPaths: (BinaryPlistUidPath | null)[] = []; + const dictionaryKeys = new Map(); + const readDictionaryKey = (object: number): string => { + const prior = dictionaryKeys.get(object); + if (prior !== undefined) return prior; + const offset = readInteger(offsetTable + object * offsetSize, offsetSize); + if (offset < 8 || offset >= objectsEnd) + throw new TypeError("binary plist object offset is invalid"); + const marker = bytes[offset] ?? 0; + const type = marker >> 4; + if (type !== 5 && type !== 6) + throw new TypeError("binary plist dictionary key is not a string"); + let size = marker & 0x0f; + let cursor = offset + 1; + if (size === 15) { + const extended = bytes[cursor] ?? 0; + if (extended >> 4 !== 1 || (extended & 15) > 3) + throw new TypeError("binary plist extended object size is invalid"); + const width = 1 << (extended & 15); + size = readInteger(cursor + 1, width); + cursor += 1 + width; + } + if (size < 0 || cursor + size * (type === 6 ? 2 : 1) > objectsEnd) + throw new TypeError("binary plist scalar object is truncated"); + let key = ""; + for (let index = 0; index < size; index += 1) + key += String.fromCharCode( + type === 5 + ? bytes[cursor + index]! + : bytes.readUInt16BE(cursor + index * 2), + ); + dictionaryKeys.set(object, key); + return key; + }; let pendingObjectCount = 1; const reachableObjectOffsets = new Set(); const reachableDateOffsets = new Set(); @@ -371,6 +421,7 @@ export const estimateBinaryPlistExpansion = ( const uidWidth = (marker & 0x0f) + 1; if (offset + 1 + uidWidth > objectsEnd) throw new TypeError("binary plist UID object is truncated"); + if (entry.path !== undefined) binaryUidPaths.push(entry.path); const normalizedWidth = binaryPlistUidNormalizationWidth(bytes, offset); if (normalizedWidth !== undefined) uidWidthsToNormalize.set(offset, normalizedWidth); @@ -454,6 +505,23 @@ export const estimateBinaryPlistExpansion = ( !containersToNormalize.has(offset) ? [] : undefined; + // A duplicate dictionary key exposes only its final value in plist@5. + // Keep provenance for that value while still validating every source ref. + const valueKeys = new Map(); + if (entry.path !== undefined && type === 13) { + const lastKeyIndexes = new Map(); + for (let index = 0; index < size; index += 1) { + const reference = readInteger( + cursor + index * referenceSize, + referenceSize, + ); + if (reference < 0 || reference >= objectCount) + throw new TypeError("binary plist reference is invalid"); + lastKeyIndexes.set(readDictionaryKey(reference), index); + } + for (const [key, index] of lastKeyIndexes) + if (key !== PROTOTYPE_KEY) valueKeys.set(size + index, key); + } active.add(entry.object); pending.push({ object: entry.object, @@ -474,6 +542,14 @@ export const estimateBinaryPlistExpansion = ( depth: entry.depth + 1, exit: false, dictionaryKey: type === 13 && index < size, + ...(entry.path === undefined || (type === 13 && !valueKeys.has(index)) + ? {} + : { + path: { + parent: entry.path, + key: type === 13 ? valueKeys.get(index)! : index, + }, + }), }); } if (parsedReferences !== undefined) @@ -527,6 +603,7 @@ export const estimateBinaryPlistExpansion = ( } return { estimatedBytes: estimate, + ...(uidProjection ? { binaryUidPaths } : {}), binaryPrototypeKeyObserved, datePrecisionOverlapCount: 0, reachableDateOffsets: [], @@ -597,6 +674,7 @@ export const estimateBinaryPlistExpansion = ( ).length; return { estimatedBytes: estimate, + ...(uidProjection ? { binaryUidPaths } : {}), binaryPrototypeKeyObserved, datePrecisionOverlapCount, reachableDateOffsets: [...reachableDateOffsets].sort( diff --git a/src/domain/apple/interfaceBuilderKeyedArchive.ts b/src/domain/apple/interfaceBuilderKeyedArchive.ts index 84ad69fcf0..2d91c81487 100644 --- a/src/domain/apple/interfaceBuilderKeyedArchive.ts +++ b/src/domain/apple/interfaceBuilderKeyedArchive.ts @@ -1,3 +1,4 @@ +import { plistUidValue } from "./plistUid.js"; import { z } from "zod"; import { jsonValueSchema } from "../jsonValue.js"; @@ -196,19 +197,10 @@ export const parseInterfaceBuilderRecords = ( }; }; -/** - * A UID decodes as a one-key `UID` (binary) or `CF$UID` (XML) dictionary with - * an integer value. Any other dictionary is ordinary archive data. - */ +/** Archive references retain their binary marker identity through plist projection. */ const archiveUid = (value: unknown): number | undefined => { - if (typeof value !== "object" || value === null || Array.isArray(value)) - return undefined; - if (Object.keys(value).length !== 1) return undefined; - const item = record(value); - const marker = Object.hasOwn(item, "UID") ? item.UID : item["CF$UID"]; - return typeof marker === "number" && Number.isInteger(marker) - ? marker - : undefined; + const uid = plistUidValue(value); + return uid !== undefined && Number.isInteger(uid) ? uid : undefined; }; const HIERARCHY_KEY = diff --git a/src/domain/apple/keyedArchive.ts b/src/domain/apple/keyedArchive.ts index 4a7e5ae34c..97795c346b 100644 --- a/src/domain/apple/keyedArchive.ts +++ b/src/domain/apple/keyedArchive.ts @@ -3,6 +3,7 @@ import { bundleResourceBoundResultSchema } from "./bundleResourceRoot.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { jsonValueSchema, type JsonValue } from "../jsonValue.js"; import { localPathStringSchema } from "../localPath.js"; +import { plistUidValue } from "./plistUid.js"; import { projectPlistValue } from "./plistValue.js"; /** @@ -85,20 +86,13 @@ const record = ( !wrappedScalars.has(value) ? value : undefined; -/** - * A UID decodes as a one-key `UID` (binary) or `CF$UID` (XML) dictionary with - * a numeric value. Any other value means an ordinary dictionary, such as a - * `$top` that holds a root encoded under the key `UID`. - */ +/** Only producer-marked binary UIDs or the XML UID convention form references. */ const uid = ( value: JsonValue | undefined, wrappedScalars: WeakSet, ): number | undefined => { const object = record(value, wrappedScalars); - if (object === undefined || Object.keys(object).length !== 1) - return undefined; - const marker = Object.hasOwn(object, "UID") ? object.UID : object["CF$UID"]; - return typeof marker === "number" ? marker : undefined; + return object === undefined ? undefined : plistUidValue(object); }; const archiverMismatch = ( diff --git a/src/domain/apple/plistUid.ts b/src/domain/apple/plistUid.ts new file mode 100644 index 0000000000..7e328b9e2a --- /dev/null +++ b/src/domain/apple/plistUid.ts @@ -0,0 +1,32 @@ +/** Serialized binary UID identity is separate from an ordinary dictionary's keys. */ +const binaryUidValues = new WeakMap(); + +/** The binary decoder marks every dictionary; only type-8 records carry a UID. */ +export const markBinaryPlistDictionary = ( + value: object, + uid?: number, +): void => { + binaryUidValues.set(value, uid); +}; + +/** Preserve source identity through the single JSON projection owner. */ +export const copyPlistUidIdentity = ( + source: object, + projected: object, +): void => { + if (binaryUidValues.has(source)) + binaryUidValues.set(projected, binaryUidValues.get(source)); +}; + +/** XML/untyped decoded archives use Foundation's dictionary UID convention. */ +export const plistUidValue = (value: unknown): number | undefined => { + if (value === null || typeof value !== "object" || Array.isArray(value)) + return undefined; + if (binaryUidValues.has(value)) return binaryUidValues.get(value); + const fields = Object.keys(value); + if (fields.length !== 1) return undefined; + const key = fields[0]; + if (key !== "UID" && key !== "CF$UID") return undefined; + const descriptor = Object.getOwnPropertyDescriptor(value, key); + return typeof descriptor?.value === "number" ? descriptor.value : undefined; +}; diff --git a/src/domain/apple/plistValue.ts b/src/domain/apple/plistValue.ts index 4b2a276ebf..c2dc106d7e 100644 --- a/src/domain/apple/plistValue.ts +++ b/src/domain/apple/plistValue.ts @@ -2,6 +2,7 @@ import { types } from "node:util"; import { z } from "zod"; import type { JsonValue } from "../jsonValue.js"; +import { copyPlistUidIdentity } from "./plistUid.js"; import { UnknownPlistScalar } from "./unknownPlistScalar.js"; /** @@ -140,7 +141,9 @@ export const projectPlistValue = ( ); entries.push([key, project(descriptor.value, depth + 1)]); } - return Object.fromEntries(entries); + const result = Object.fromEntries(entries); + copyPlistUidIdentity(item, result); + return result; } return z.union([z.string(), z.number(), z.boolean(), z.null()]).parse(item); }; diff --git a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts index f6e7ac3452..219d22a7dd 100644 --- a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts +++ b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts @@ -1,3 +1,4 @@ +import { binaryKeyedArchiveUidShapes } from "../../fixtures/keyedArchiveUidShapes.js"; import { execFile } from "node:child_process"; import { mkdir, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -2165,3 +2166,21 @@ it("reports a plain plist named nib as undecoded while retaining a valid empty a reason: "one_or_more_archives_invalid", }); }); + +it("preserves authored binary UID-shaped dictionaries through UI archive decoding", async () => { + const bundle = await createTestTempDirectory("rea-ui-uid-shapes-"); + await writeFile(join(bundle, "Main.nib"), binaryKeyedArchiveUidShapes()); + const result = await analyzeInterfaceBuilderBundle({ + bundlePath: bundle, + targetSha256: "a".repeat(64), + }); + const source = result.graph.nodes.find( + (node) => node.attributes.ordinary !== undefined, + ); + expect(source).toBeDefined(); + expect(source?.attributes).toMatchObject({ + ordinary: { UID: 2 }, + xmlConvention: { CF$UID: 2 }, + actual: { archiveUID: 2, className: "NSView" }, + }); +}); diff --git a/tests/fixtures/keyedArchiveUidShapes.ts b/tests/fixtures/keyedArchiveUidShapes.ts new file mode 100644 index 0000000000..e98c06919a --- /dev/null +++ b/tests/fixtures/keyedArchiveUidShapes.ts @@ -0,0 +1,45 @@ +import { buildBinary } from "plist"; +import { + readBinaryPlistObjectOffset, + readBinaryPlistObjectTableLayout, +} from "./binaryPlistObjectTable.js"; + +/** Authored UID-shaped dictionaries coexist with actual binary type-8 records. */ +export const binaryKeyedArchiveUidShapes = (): Buffer => { + // plist's builder treats a sole UID key as a typed record. Encode ordinary + // dictionaries under same-width placeholder keys, then change only those + // serialized string records; dictionary markers and references stay intact. + const bytes = Buffer.from( + buildBinary({ + $archiver: "NSKeyedArchiver", + $objects: [ + "$null", + { + $class: { UID: 3 }, + ordinary: { raw: 2 }, + xmlConvention: { cf_uid: 2 }, + actual: { UID: 2 }, + shared: [{ UID: 2 }, { UID: 2 }], + }, + { $class: { UID: 3 }, title: "Destination" }, + { $classname: "NSView", $classes: ["NSView", "NSObject"] }, + ], + $top: { root: { UID: 1 } }, + }), + ); + const table = readBinaryPlistObjectTableLayout(bytes); + for (let id = 0; id < table.objectCount; id += 1) { + const offset = readBinaryPlistObjectOffset(bytes, table, id); + if ( + bytes[offset] === 0x53 && + bytes.toString("ascii", offset + 1, offset + 4) === "raw" + ) + bytes.write("UID", offset + 1, "ascii"); + if ( + bytes[offset] === 0x56 && + bytes.toString("ascii", offset + 1, offset + 7) === "cf_uid" + ) + bytes.write("CF$UID", offset + 1, "ascii"); + } + return bytes; +};