can1357/oh-my-pi · error · ArchiveError

Archive symlink '${formatArchivePathForError(memberPath)}' c

Error message

Archive symlink '${formatArchivePathForError(memberPath)}' cannot be materialized from target '${formatArchivePathForError(targetPath)}'

What it means

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).

Source

Thrown at packages/utils/src/ar/reader.ts:15

import { ensureParentDirectories, resolveArchiveLinkPath, upsertArchiveEntry } from "./entries";
import { ArchiveError } from "./error";
import { type ArchiveLimits, assertArchiveMemberSize, DEFAULT_ARCHIVE_LIMITS } from "./limits";
import { formatArchivePathForError, normalizeArchiveLookupPath } from "./paths";
import type {
	ArchiveDirectoryEntry,
	ArchiveFormat,
	ArchiveIndexEntry,
	ArchiveNode,
	ExtractedArchiveFile,
} from "./types";

/** Raise the canonical error for a symlink whose target cannot be materialized. */
export function throwUnreadableArchiveLink(targetPath: string, memberPath: string): never {
	throw new ArchiveError(
		`Archive symlink '${formatArchivePathForError(memberPath)}' cannot be materialized from target '${formatArchivePathForError(targetPath)}'`,
	);
}

/**
 * An indexed, read-only view over a single archive. Member payloads stay
 * lazy behind their format's `MemberSource`; symlink aliases are traversed
 * lazily so N files behind M directory aliases never inflate the index to
 * N×M entries during listing.
 */
export class ArchiveReader {
	readonly format: ArchiveFormat;
	readonly limits: ArchiveLimits;
	#entries = new Map<string, ArchiveIndexEntry>();

	constructor(format: ArchiveFormat, entries: ArchiveIndexEntry[], limits: ArchiveLimits = DEFAULT_ARCHIVE_LIMITS) {
		this.format = format;
		this.limits = limits;

View on GitHub (pinned to 9690622007)

Solutions

  1. 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.
  2. Fix dangling symlinks in the source directory before archiving (e.g. `find . -xtype l -delete` or repoint them).
  3. If the target legitimately lives outside the archive, copy the real file into the archive rather than linking.
  4. Catch this ArchiveError around readFile and treat dead links as skippable members, logging the memberPath/targetPath from the message.

Example fix

// before: throws on a dangling archive symlink
const file = await reader.readFile('lib/current.so');

// after: guard against unmaterializable links
const entry = reader.allEntries().find(e => e.path === 'lib/current.so');
if (entry?.isSymlink) {
  const target = entry.linkTarget ?? '';
  if (!reader.allEntries().some(e => e.path === target)) {
    throw new Error(`Skipping broken link: ${entry.path} -> ${target}`);
  }
}
const file = await reader.readFile('lib/current.so');
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify a symlink's target exists among archive entries before reading it
const entries = reader.allEntries();
const link = entries.find(e => e.path === memberPath);
if (link?.isSymlink && !entries.some(e => e.path === link.linkTarget)) {
  throw new Error(`Dangling archive symlink: ${memberPath} -> ${link.linkTarget}`);
}

Type guard

function isReadableSymlink(
  entries: { path: string; isDirectory: boolean; isSymlink?: boolean; linkTarget?: string }[],
  memberPath: string,
): boolean {
  const link = entries.find(e => e.path === memberPath);
  if (!link?.isSymlink) return true;
  return entries.some(e => e.path === link.linkTarget && !e.isDirectory);
}

Try / catch

try {
  return await reader.readFile(memberPath);
} catch (err) {
  if (err instanceof ArchiveError && err.message.startsWith("Archive symlink '")) {
    logger.warn('Skipping unmaterializable archive symlink', { memberPath });
    return null; // treat as skippable member
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/bd4a64760f78446b. Report an issue: GitHub.