{"record":{"id":"2e2f30a86c8c1c2a","repo":"can1357/oh-my-pi","slug":"unterminated-ar-archive-long-name-at-offset-offs","errorCode":null,"errorMessage":"Unterminated ar archive long name at offset ${offset}","messagePattern":"Unterminated ar archive long name at offset (.+?)","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/unix-ar.ts","lineNumber":137,"sourceCode":"function shortName(rawName: string): string {\n\tif (rawName === \"/\" || rawName === \"//\" || rawName === \"/SYM64/\") return rawName;\n\treturn rawName.endsWith(\"/\") ? rawName.slice(0, -1) : rawName;\n}\n\nfunction resolveLongName(\n\treference: string,\n\ttable: Uint8Array,\n\tlimits: ArchiveLimits,\n): { name: string; byteLength: number } {\n\tconst offsetText = reference.slice(1);\n\tif (!/^\\d+$/.test(offsetText)) throw new ArchiveError(`Invalid ar archive member name '${reference}'`);\n\tconst offset = Number.parseInt(offsetText, 10);\n\tif (!Number.isSafeInteger(offset) || offset < 0 || offset >= table.byteLength) {\n\t\tthrow new ArchiveError(`Invalid ar archive long-name offset '${reference}'`);\n\t}\n\tlet end = offset;\n\twhile (end < table.byteLength && table[end] !== 0 && table[end] !== 0x0a) end++;\n\tif (end === table.byteLength) throw new ArchiveError(`Unterminated ar archive long name at offset ${offset}`);\n\tlet nameEnd = end;\n\tif (table[end] === 0x0a && nameEnd > offset && table[nameEnd - 1] === 0x2f) nameEnd--;\n\tif (nameEnd === offset) throw new ArchiveError(`Empty ar archive long name at offset ${offset}`);\n\tconst nameBytes = table.subarray(offset, nameEnd);\n\treturn { name: decodeName(nameBytes, limits), byteLength: nameBytes.byteLength };\n}\n\nfunction isMetadataName(name: string): boolean {\n\treturn name === \"/\" || name === \"//\" || name === \"/SYM64/\" || name === \"__.SYMDEF\" || name === \"__.SYMDEF SORTED\";\n}\n\nfunction materializeEntries(\n\trecords: RawArMember[],\n\tlongNames: Uint8Array | undefined,\n\tsource: ByteSource,\n\toptions: FormatReadOptions,\n): ArchiveIndexEntry[] {\n\tconst entries = new Map<string, ArchiveIndexEntry>();","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/unix-ar.ts#L119-L155","documentation":"resolveLongName scans the string table from the given offset for a terminator (NUL or newline); if it reaches the end of the table without finding one, it throws 'Unterminated ar archive long name at offset <n>'. GNU ar terminates every table entry with '/\\n' (or '/' + NUL), so a missing terminator means the '// ' table is malformed or truncated mid-entry.","triggerScenarios":"Parsing an archive whose string table's final entry lacks its trailing terminator because the '// ' member was truncated by exactly the terminator bytes, or a custom writer forgot to append '/\\n' after the last name.","commonSituations":"Truncated downloads cutting the tail of the archive; custom writers concatenating names without separators; tools that strip trailing newlines from archive members.","solutions":["Ensure the producer appends '/\\n' after every name in the '// ' table, including the last one","Verify the string-table member's size field includes all terminators and pads the table to even size","Regenerate the archive with GNU ar / llvm-ar","Compare the declared table size against actual table bytes to locate the truncation"],"exampleFix":"// before: last name not terminated\ntable = Buffer.concat([table, Buffer.from('lastName')]);\n// after: terminate every entry\nfor (const n of names) table = Buffer.concat([table, Buffer.from(n + '/\\n')]);","handlingStrategy":"try-catch","validationCode":"function stringTableWellFormed(table: Uint8Array): boolean {\n  // every GNU entry ends with '/' followed by \\n or NUL\n  if (table.byteLength === 0) return true;\n  const last = table[table.byteLength - 1];\n  return last === 0x0a || last === 0x00;\n}\n// verify on the '// ' member payload before parsing references","typeGuard":"function isTerminatedArStringTable(table: Uint8Array): boolean {\n  return table.byteLength === 0 || table[table.byteLength - 1] === 0x0a || table[table.byteLength - 1] === 0x00;\n}","tryCatchPattern":"try {\n  const entries = await archive.list();\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.startsWith('Unterminated ar archive long name')) {\n    // truncated string table: re-fetch/rebuild archive; parse offset from message for diagnostics\n  } else throw err;\n}","preventionTips":["Terminate every table entry with '/\\n', including the final one","Include terminators in the table member's size field","Pad the table to even byte length","Verify archive integrity (size/hash) after download before parsing"],"tags":["archive","parsing","ar-format","gnu","truncation"],"backgroundTag":"corrupt-archive-header","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}