can1357/oh-my-pi · error · ArchiveError

Tar member '${formatArchivePathForError(parent)}' is not a d

Error message

Tar member '${formatArchivePathForError(parent)}' is not a directory

What it means

While inserting implicit parent directory entries, the encoder found a parent path already registered as a regular file. A tar path cannot be both a file and a prefix of another member, so it throws naming the offending parent.

Source

Thrown at packages/utils/src/ar/tar.ts:756

export async function encodeTar(members: Iterable<readonly [string, Uint8Array]>): Promise<Uint8Array> {
	const parts: Uint8Array[] = [];
	const kinds = new Map<string, "directory" | "file">();
	let sequence = 0;
	for (const [rawPath, bytes] of members) {
		const directory = rawPath.endsWith("/") || rawPath.endsWith("\\");
		const normalized = normalizeArchiveEntryPath(rawPath);
		if (!normalized) throw new ArchiveError(`Invalid tar member path '${formatArchivePathForError(rawPath)}'`);
		const pathBytes = TEXT_ENCODER.encode(normalized);
		if (pathBytes.byteLength === 0) throw new ArchiveError("Invalid empty tar member path");
		if (directory && bytes.byteLength !== 0) {
			throw new ArchiveError(`Tar directory '${formatArchivePathForError(normalized)}' cannot contain file data`);
		}
		const segments = normalized.split("/");
		for (let index = 1; index < segments.length; index++) {
			const parent = segments.slice(0, index).join("/");
			const kind = kinds.get(parent);
			if (kind === "file")
				throw new ArchiveError(`Tar member '${formatArchivePathForError(parent)}' is not a directory`);
			if (kind === "directory") continue;
			kinds.set(parent, "directory");
			appendTarEntry(parts, parent, new Uint8Array(0), true, sequence++);
		}
		const existing = kinds.get(normalized);
		if (existing) {
			if (directory && existing === "directory") continue;
			throw new ArchiveError(`Duplicate tar member path '${formatArchivePathForError(normalized)}'`);
		}
		kinds.set(normalized, directory ? "directory" : "file");
		appendTarEntry(parts, normalized, bytes, directory, sequence++);
	}
	parts.push(new Uint8Array(BLOCK_SIZE * 2));
	return concatBytes(parts);
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Rename one of the conflicting members so no path is both a file and a directory prefix
  2. Drop the file entry 'a' or nest its data at another key
  3. Deduplicate/validate the members map keys before encoding

Example fix

// before
members.set("a", fileBytes);
members.set("a/b", otherBytes); // conflict
// after
members.set("a-collision", fileBytes);
members.set("a/b", otherBytes);
Defensive patterns

Strategy: validation

Validate before calling

const fileKeys = new Set(
	[...members.entries()].filter(([, b]) => b.byteLength > 0 && !/[\\/]$/.test(k0(b)))
);
function k0(x: unknown) { return x as string; }
for (const key of members.keys()) {
	const segs = key.split("/");
	for (let i = 1; i < segs.length; i++) {
		const parent = segs.slice(0, i).join("/");
		if (fileKeys.has(parent)) throw new Error(`'${parent}' is a file but '${key}' needs it as a directory`);
	}
}

Try / catch

try {
	return await createTar(members);
} catch (err) {
	if (err instanceof ArchiveError && err.message.includes("is not a directory")) {
		// rename one of the colliding members
	}
	throw err;
}

Prevention

When it happens

Trigger: Members include both 'a' (a file with bytes) and 'a/b' (a file) — the encoder must synthesize directory 'a' for 'a/b' but 'a' is already a file.

Common situations: Merging member sets from multiple sources; keys derived from filesystem scans where 'a' is a file but a stale key 'a/b' persisted; path collisions after normalization (e.g. 'a' vs './a').

Related errors


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