{"record":{"id":"bd4a64760f78446b","repo":"can1357/oh-my-pi","slug":"archive-symlink-formatarchivepathforerror-membe","errorCode":null,"errorMessage":"Archive symlink '${formatArchivePathForError(memberPath)}' cannot be materialized from target '${formatArchivePathForError(targetPath)}'","messagePattern":"Archive symlink '(.+?)' cannot be materialized from target '(.+?)'","errorType":"exception","errorClass":"ArchiveError","httpStatus":null,"severity":"error","filePath":"packages/utils/src/ar/reader.ts","lineNumber":15,"sourceCode":"import { ensureParentDirectories, resolveArchiveLinkPath, upsertArchiveEntry } from \"./entries\";\nimport { ArchiveError } from \"./error\";\nimport { type ArchiveLimits, assertArchiveMemberSize, DEFAULT_ARCHIVE_LIMITS } from \"./limits\";\nimport { formatArchivePathForError, normalizeArchiveLookupPath } from \"./paths\";\nimport type {\n\tArchiveDirectoryEntry,\n\tArchiveFormat,\n\tArchiveIndexEntry,\n\tArchiveNode,\n\tExtractedArchiveFile,\n} from \"./types\";\n\n/** Raise the canonical error for a symlink whose target cannot be materialized. */\nexport function throwUnreadableArchiveLink(targetPath: string, memberPath: string): never {\n\tthrow new ArchiveError(\n\t\t`Archive symlink '${formatArchivePathForError(memberPath)}' cannot be materialized from target '${formatArchivePathForError(targetPath)}'`,\n\t);\n}\n\n/**\n * An indexed, read-only view over a single archive. Member payloads stay\n * lazy behind their format's `MemberSource`; symlink aliases are traversed\n * lazily so N files behind M directory aliases never inflate the index to\n * N×M entries during listing.\n */\nexport class ArchiveReader {\n\treadonly format: ArchiveFormat;\n\treadonly limits: ArchiveLimits;\n\t#entries = new Map<string, ArchiveIndexEntry>();\n\n\tconstructor(format: ArchiveFormat, entries: ArchiveIndexEntry[], limits: ArchiveLimits = DEFAULT_ARCHIVE_LIMITS) {\n\t\tthis.format = format;\n\t\tthis.limits = limits;","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/utils/src/ar/reader.ts#L1-L33","documentation":"Canonical error for a symlink entry inside an archive whose target path cannot be resolved to a real member, or cannot otherwise be materialized. `throwUnreadableArchiveLink` is raised from `readFile` when following an archive symlink/alias hits a dead end, and reports both the symlink's path and the unresolved target (packages/utils/src/ar/reader.ts:15).","triggerScenarios":"Calling `reader.readFile(path)` where `path` resolves (through one or more symlinks, up to limits.maxLinkDepth) to a target that does not exist among the archive entries, or the link chain terminates without a concrete file entry.","commonSituations":"Archives created from directories containing dangling symlinks (broken dev environments, node_modules links, /etc/alternatives-style links); tar/zip pickled with absolute symlink targets that don't exist inside the archive; link chains pointing outside the archive root.","solutions":["Inspect the archive listing (`reader.allEntries()`/`listDirectory`) and check whether the link's target entry actually exists; re-create the archive with the target included or the link removed.","Fix dangling symlinks in the source directory before archiving (e.g. `find . -xtype l -delete` or repoint them).","If the target legitimately lives outside the archive, copy the real file into the archive rather than linking.","Catch this ArchiveError around readFile and treat dead links as skippable members, logging the memberPath/targetPath from the message."],"exampleFix":"// before: throws on a dangling archive symlink\nconst file = await reader.readFile('lib/current.so');\n\n// after: guard against unmaterializable links\nconst entry = reader.allEntries().find(e => e.path === 'lib/current.so');\nif (entry?.isSymlink) {\n  const target = entry.linkTarget ?? '';\n  if (!reader.allEntries().some(e => e.path === target)) {\n    throw new Error(`Skipping broken link: ${entry.path} -> ${target}`);\n  }\n}\nconst file = await reader.readFile('lib/current.so');","handlingStrategy":"try-catch","validationCode":"// Verify a symlink's target exists among archive entries before reading it\nconst entries = reader.allEntries();\nconst link = entries.find(e => e.path === memberPath);\nif (link?.isSymlink && !entries.some(e => e.path === link.linkTarget)) {\n  throw new Error(`Dangling archive symlink: ${memberPath} -> ${link.linkTarget}`);\n}","typeGuard":"function isReadableSymlink(\n  entries: { path: string; isDirectory: boolean; isSymlink?: boolean; linkTarget?: string }[],\n  memberPath: string,\n): boolean {\n  const link = entries.find(e => e.path === memberPath);\n  if (!link?.isSymlink) return true;\n  return entries.some(e => e.path === link.linkTarget && !e.isDirectory);\n}","tryCatchPattern":"try {\n  return await reader.readFile(memberPath);\n} catch (err) {\n  if (err instanceof ArchiveError && err.message.startsWith(\"Archive symlink '\")) {\n    logger.warn('Skipping unmaterializable archive symlink', { memberPath });\n    return null; // treat as skippable member\n  }\n  throw err;\n}","preventionTips":["Remove or fix dangling symlinks in source directories before archiving (`find . -xtype l`).","Prefer storing real file copies over symlinks in distributed archives.","Avoid absolute or outside-root symlink targets when creating archives.","List and audit symlink entries (path + target) when ingesting third-party archives."],"tags":["archive","symlink","broken-link"],"backgroundTag":"broken-symlink","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}