Files
dockhand-upstream/src/lib/server/tar-extract.ts
T
2026-06-17 08:21:38 +02:00

65 lines
2.1 KiB
TypeScript

/**
* Single-file tar extraction for raw downloads (#1180).
*
* Docker's /archive endpoint always wraps file contents in a USTAR tar.
* When the user picks the "no archive" download format and the path is a
* regular file, we strip the wrapper and emit the bytes verbatim.
*
* Only handles the first regular-file entry — the caller has already
* guaranteed (via stat) that the requested path is a single file, so the
* tar contains exactly one entry.
*/
/**
* Extract the bytes of the first regular file entry in a USTAR tar.
* Returns the file content as a Uint8Array.
*
* Throws when no regular file entry is found (e.g. the tar contained only
* a directory header) — that's an unexpected state, since the caller is
* supposed to have already verified the path points to a file.
*/
export function extractFirstFileFromTar(tarData: Uint8Array): Uint8Array {
let offset = 0;
while (offset + 512 <= tarData.length) {
const header = tarData.subarray(offset, offset + 512);
// Two consecutive zero blocks mark end-of-archive.
if (isZeroBlock(header)) break;
const name = readString(header, 0, 100);
const sizeOctal = readString(header, 124, 12).trim();
const size = sizeOctal ? parseInt(sizeOctal, 8) : 0;
const typeFlag = header[156];
// Regular file: typeflag '0' (0x30) or NUL (0x00, legacy)
const isRegularFile = typeFlag === 0x30 || typeFlag === 0x00;
if (isRegularFile && name && size >= 0) {
const start = offset + 512;
const end = start + size;
if (end > tarData.length) {
throw new Error('Truncated tar archive');
}
return tarData.subarray(start, end);
}
// Skip header + content (padded to 512-byte boundary)
offset += 512 + Math.ceil(size / 512) * 512;
}
throw new Error('No regular file entry found in tar archive');
}
function isZeroBlock(block: Uint8Array): boolean {
for (let i = 0; i < block.length; i++) {
if (block[i] !== 0) return false;
}
return true;
}
function readString(buf: Uint8Array, offset: number, length: number): string {
let end = offset;
const limit = offset + length;
while (end < limit && buf[end] !== 0) end++;
return new TextDecoder('utf-8').decode(buf.subarray(offset, end));
}