import { FileTypes, readBlock, recordSize, type TarMeta, ustarStructure,} from "./_common.ts";import { readAll } from "../streams/read_all.ts";import type { Reader } from "../types.d.ts";
export interface TarHeader { [key: string]: Uint8Array;}
const initialChecksum = 8 * 32;
function trim(buffer: Uint8Array): Uint8Array { const index = buffer.findIndex((v): boolean => v === 0); if (index < 0) return buffer; return buffer.subarray(0, index);}
function parseHeader(buffer: Uint8Array): { [key: string]: Uint8Array } { const data: { [key: string]: Uint8Array } = {}; let offset = 0; ustarStructure.forEach(function (value) { const arr = buffer.subarray(offset, offset + value.length); data[value.field] = arr; offset += value.length; }); return data;}
export interface TarEntry extends TarMeta {}
export class TarEntry implements Reader { #header: TarHeader; #reader: Reader | (Reader & Deno.Seeker); #size: number; #read = 0; #consumed = false; #entrySize: number; constructor( meta: TarMeta, header: TarHeader, reader: Reader | (Reader & Deno.Seeker), ) { Object.assign(this, meta); this.#header = header; this.#reader = reader;
this.#size = this.fileSize || 0; const blocks = Math.ceil(this.#size / recordSize); this.#entrySize = blocks * recordSize; }
get consumed(): boolean { return this.#consumed; }
async read(p: Uint8Array): Promise<number | null> { const entryBytesLeft = this.#entrySize - this.#read; const bufSize = Math.min( p.length, entryBytesLeft, );
if (entryBytesLeft <= 0) { this.#consumed = true; return null; }
const block = new Uint8Array(bufSize); const n = await readBlock(this.#reader, block); const bytesLeft = this.#size - this.#read;
this.#read += n || 0; if (n === null || bytesLeft <= 0) { if (n === null) this.#consumed = true; return null; }
const offset = bytesLeft < n ? bytesLeft : n; p.set(block.subarray(0, offset), 0);
return offset < 0 ? n - Math.abs(offset) : offset; }
async discard() { if (this.#consumed) return; this.#consumed = true;
if (typeof (this.#reader as Deno.Seeker).seek === "function") { await (this.#reader as Deno.Seeker).seek( this.#entrySize - this.#read, Deno.SeekMode.Current, ); this.#read = this.#entrySize; } else { await readAll(this); } }}
export class Untar { reader: Reader; block: Uint8Array; #entry: TarEntry | undefined;
constructor(reader: Reader) { this.reader = reader; this.block = new Uint8Array(recordSize); }
#checksum(header: Uint8Array): number { let sum = initialChecksum; for (let i = 0; i < 512; i++) { if (i >= 148 && i < 156) { continue; } sum += header[i]; } return sum; }
async #getHeader(): Promise<TarHeader | null> { await readBlock(this.reader, this.block); const header = parseHeader(this.block);
const decoder = new TextDecoder(); const checksum = this.#checksum(this.block);
if (parseInt(decoder.decode(header.checksum), 8) !== checksum) { if (checksum === initialChecksum) { return null; } throw new Error("checksum error"); }
const magic = decoder.decode(header.ustar);
if (magic.indexOf("ustar")) { throw new Error(`unsupported archive format: ${magic}`); }
return header; }
#getMetadata(header: TarHeader): TarMeta { const decoder = new TextDecoder(); const meta: TarMeta = { fileName: decoder.decode(trim(header.fileName)), }; const fileNamePrefix = trim(header.fileNamePrefix); if (fileNamePrefix.byteLength > 0) { meta.fileName = decoder.decode(fileNamePrefix) + "/" + meta.fileName; } (["fileMode", "mtime", "uid", "gid"] as [ "fileMode", "mtime", "uid", "gid", ]).forEach((key) => { const arr = trim(header[key]); if (arr.byteLength > 0) { meta[key] = parseInt(decoder.decode(arr), 8); } }); (["owner", "group", "type"] as ["owner", "group", "type"]).forEach( (key) => { const arr = trim(header[key]); if (arr.byteLength > 0) { meta[key] = decoder.decode(arr); } }, );
meta.fileSize = parseInt(decoder.decode(header.fileSize), 8); meta.type = FileTypes[parseInt(meta.type!)] ?? meta.type;
return meta; }
async extract(): Promise<TarEntry | null> { if (this.#entry && !this.#entry.consumed) { await this.#entry.discard(); }
const header = await this.#getHeader(); if (header === null) return null;
const meta = this.#getMetadata(header);
this.#entry = new TarEntry(meta, header, this.reader);
return this.#entry; }
async *[Symbol.asyncIterator](): AsyncIterableIterator<TarEntry> { while (true) { const entry = await this.extract();
if (entry === null) return;
yield entry; } }}