can1357/oh-my-pi · error · ArchiveError

Invalid ZIP archive: symlink has no payload

Error message

Invalid ZIP archive: symlink has no payload

What it means

Thrown when a central-directory entry is detected as a symlink but its storage type is not a regular member payload, so the library cannot read the symlink target bytes to validate them. It refuses to construct an entry with an unverifiable target.

Source

Thrown at packages/utils/src/ar/zip.ts:554

			};
			parsed.push({ entry, isSymlink });
			assertEntryCount(parsed.length, options.limits);
		}
		offset = end;
	}
	return parsed;
}

async function readZipImpl(source: ByteSource, options: FormatReadOptions): Promise<ArchiveIndexEntry[]> {
	const info = await readCentralDirectoryInfo(source, options.limits);
	if (info.size === 0) return [];
	const directory = await source.read(info.offset, info.offset + info.size);
	if (directory.byteLength !== info.size) throw new ArchiveError("Invalid ZIP archive: truncated central directory");
	const parsed = parseCentralDirectory(source, directory, info, options);
	for (const item of parsed) {
		if (!item.isSymlink) continue;
		assertArchivePathBytes(item.entry.size, "symlink target", options.limits.maxPathBytes);
		if (item.entry.storage?.type !== "member") throw new ArchiveError("Invalid ZIP archive: symlink has no payload");
		const bytes = await item.entry.storage.source.read(item.entry.size, item.entry.path);
		const target = UTF8_DECODER.decode(bytes);
		assertArchivePathString(target, "symlink target", options.limits.maxPathBytes);
		const portable = target.replace(/\\/g, "/");
		const absolute = path.posix.isAbsolute(portable) || /^[A-Za-z]:/.test(portable) || portable.includes("\0");
		const targetPath = absolute
			? undefined
			: normalizeArchiveLookupPath(path.posix.join(path.posix.dirname(item.entry.path), portable));
		item.entry.size = 0;
		item.entry.storage = {
			type: "link",
			targetPath: targetPath === undefined ? portable : targetPath,
			resolveTarget: targetPath !== undefined,
		};
	}
	return parsed.map(item => item.entry);
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Recreate the archive with a zip tool that stores symlinks correctly (e.g. `zip -ry archive.zip dir` preserves symlinks)
  2. Inspect the archive with `unzip -v` / 7-Zip to find the malformed symlink entry and repair or drop it
  3. If you only need regular files, filter out symlinks before/while reading if the API allows, or extract with a tolerant tool first

Example fix

// before: zipping with a tool that breaks symlinks
await $`zip -r app.zip app`;
// after: preserve symlinks recursively
await $`zip -ry app.zip app`;
Defensive patterns

Strategy: try-catch

Validate before calling

const entries = await listZip(file); // if listing API available
const bad = entries.filter(e => e.isSymlink && (e.size === 0 || e.storageType !== 'member'));
if (bad.length) throw new Error(`malformed symlinks: ${bad.map(b => b.path).join(', ')}`);

Try / catch

try {
  return await readZip(file);
} catch (err) {
  if (err instanceof ArchiveError && err.message.includes('symlink has no payload')) {
    logger.warn('skipping archive with malformed symlink entries', { path: file.name });
    return null;
  }
  throw err;
}

Prevention

When it happens

Trigger: Reading a ZIP containing a symlink entry (external attributes mark it as a symlink) whose storage is missing or of a non-member type — typically a corrupt entry, a directory placeholder, or a zero-size/dangling symlink record.

Common situations: ZIPs of Unix directory trees created by tools that mishandle symlinks (some tar-to-zip converters, misconfigured CI packaging); archives truncated or patched so the symlink's data offset no longer resolves.

Related errors


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