diff options
Diffstat (limited to 'std/archive/tar.ts')
-rw-r--r-- | std/archive/tar.ts | 263 |
1 files changed, 209 insertions, 54 deletions
diff --git a/std/archive/tar.ts b/std/archive/tar.ts index d549a4623..8ec240764 100644 --- a/std/archive/tar.ts +++ b/std/archive/tar.ts @@ -27,16 +27,42 @@ * THE SOFTWARE. */ import { MultiReader } from "../io/readers.ts"; -import { BufReader } from "../io/bufio.ts"; +import { PartialReadError } from "../io/bufio.ts"; import { assert } from "../_util/assert.ts"; +type Reader = Deno.Reader; +type Seeker = Deno.Seeker; + const recordSize = 512; const ustar = "ustar\u000000"; +// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06 +// eight checksum bytes taken to be ascii spaces (decimal value 32) +const initialChecksum = 8 * 32; + +async function readBlock( + reader: Deno.Reader, + p: Uint8Array +): Promise<number | null> { + let bytesRead = 0; + while (bytesRead < p.length) { + const rr = await reader.read(p.subarray(bytesRead)); + if (rr === null) { + if (bytesRead === 0) { + return null; + } else { + throw new PartialReadError(); + } + } + bytesRead += rr; + } + return bytesRead; +} + /** * Simple file reader */ -class FileReader implements Deno.Reader { +class FileReader implements Reader { private file?: Deno.File; constructor(private filePath: string) {} @@ -79,24 +105,34 @@ function pad(num: number, bytes: number, base?: number): string { return "000000000000".substr(numString.length + 12 - bytes) + numString; } +const types: { [key: string]: string } = { + "": "file", + "0": "file", + "1": "link", + "2": "symlink", + "3": "character-device", + "4": "block-device", + "5": "directory", +}; + /* struct posix_header { // byte offset - char name[100]; // 0 - char mode[8]; // 100 - char uid[8]; // 108 - char gid[8]; // 116 - char size[12]; // 124 - char mtime[12]; // 136 - char chksum[8]; // 148 - char typeflag; // 156 - char linkname[100]; // 157 - char magic[6]; // 257 - char version[2]; // 263 - char uname[32]; // 265 - char gname[32]; // 297 - char devmajor[8]; // 329 - char devminor[8]; // 337 - char prefix[155]; // 345 + char name[100]; // 0 + char mode[8]; // 100 + char uid[8]; // 108 + char gid[8]; // 116 + char size[12]; // 124 + char mtime[12]; // 136 + char chksum[8]; // 148 + char typeflag; // 156 + char linkname[100]; // 157 + char magic[6]; // 257 + char version[2]; // 263 + char uname[32]; // 265 + char gname[32]; // 297 + char devmajor[8]; // 329 + char devminor[8]; // 337 + char prefix[155]; // 345 // 500 }; */ @@ -198,6 +234,10 @@ function parseHeader(buffer: Uint8Array): { [key: string]: Uint8Array } { return data; } +interface TarHeader { + [key: string]: Uint8Array; +} + export interface TarData { fileName?: string; fileNamePrefix?: string; @@ -221,7 +261,7 @@ export interface TarDataWithSource extends TarData { /** * buffer to read */ - reader?: Deno.Reader; + reader?: Reader; } export interface TarInfo { @@ -231,6 +271,7 @@ export interface TarInfo { gid?: number; owner?: string; group?: string; + type?: string; } export interface TarOptions extends TarInfo { @@ -242,7 +283,7 @@ export interface TarOptions extends TarInfo { /** * append any arbitrary content */ - reader?: Deno.Reader; + reader?: Reader; /** * size of the content to be appended @@ -250,10 +291,14 @@ export interface TarOptions extends TarInfo { contentSize?: number; } -export interface UntarOptions extends TarInfo { +export interface TarMeta extends TarInfo { fileName: string; + fileSize?: number; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface TarEntry extends TarMeta {} + /** * A class to create a tar archive */ @@ -364,8 +409,8 @@ export class Tar { /** * Get a Reader instance for this tar data */ - getReader(): Deno.Reader { - const readers: Deno.Reader[] = []; + getReader(): Reader { + const readers: Reader[] = []; this.data.forEach((tarData): void => { let { reader } = tarData; const { filePath } = tarData; @@ -395,44 +440,132 @@ export class Tar { } } +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; + + // File Size + this.#size = this.fileSize || 0; + // Entry Size + const blocks = Math.ceil(this.#size / recordSize); + this.#entrySize = blocks * recordSize; + } + + get consumed(): boolean { + return this.#consumed; + } + + async read(p: Uint8Array): Promise<number | null> { + // Bytes left for entry + const entryBytesLeft = this.#entrySize - this.#read; + const bufSize = Math.min( + // bufSize can't be greater than p.length nor bytes left in the entry + p.length, + entryBytesLeft + ); + + if (entryBytesLeft <= 0) 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 (null) this.#consumed = true; + return null; + } + + // Remove zero filled + const offset = bytesLeft < n ? bytesLeft : n; + p.set(block.subarray(0, offset), 0); + + return offset < 0 ? n - Math.abs(offset) : offset; + } + + async discard(): Promise<void> { + // Discard current entry + if (this.#consumed) return; + this.#consumed = true; + + if (typeof (this.#reader as Seeker).seek === "function") { + await (this.#reader as Seeker).seek( + this.#entrySize - this.#read, + Deno.SeekMode.Current + ); + this.#read = this.#entrySize; + } else { + await Deno.readAll(this); + } + } +} + /** - * A class to create a tar archive + * A class to extract a tar archive */ export class Untar { - reader: BufReader; + reader: Reader; block: Uint8Array; + #entry: TarEntry | undefined; - constructor(reader: Deno.Reader) { - this.reader = new BufReader(reader); + constructor(reader: Reader) { + this.reader = reader; this.block = new Uint8Array(recordSize); } - async extract(writer: Deno.Writer): Promise<UntarOptions> { - await this.reader.readFull(this.block); + #checksum = (header: Uint8Array): number => { + let sum = initialChecksum; + for (let i = 0; i < 512; i++) { + if (i >= 148 && i < 156) { + // Ignore checksum header + continue; + } + sum += header[i]; + } + return sum; + }; + + #getHeader = async (): Promise<TarHeader | null> => { + await readBlock(this.reader, this.block); const header = parseHeader(this.block); // calculate the checksum - let checksum = 0; - const encoder = new TextEncoder(), - decoder = new TextDecoder("ascii"); - Object.keys(header) - .filter((key): boolean => key !== "checksum") - .forEach(function (key): void { - checksum += header[key].reduce((p, c): number => p + c, 0); - }); - checksum += encoder.encode(" ").reduce((p, c): number => p + c, 0); + const decoder = new TextDecoder(); + const checksum = this.#checksum(this.block); if (parseInt(decoder.decode(header.checksum), 8) !== checksum) { + if (checksum === initialChecksum) { + // EOF + return null; + } throw new Error("checksum error"); } const magic = decoder.decode(header.ustar); - if (magic !== ustar) { + + if (magic.indexOf("ustar")) { throw new Error(`unsupported archive format: ${magic}`); } + return header; + }; + + #getMetadata = (header: TarHeader): TarMeta => { + const decoder = new TextDecoder(); // get meta data - const meta: UntarOptions = { + const meta: TarMeta = { fileName: decoder.decode(trim(header.fileName)), }; const fileNamePrefix = trim(header.fileNamePrefix); @@ -450,23 +583,45 @@ export class Untar { meta[key] = parseInt(decoder.decode(arr), 8); } }); - (["owner", "group"] as ["owner", "group"]).forEach((key): void => { - const arr = trim(header[key]); - if (arr.byteLength > 0) { - meta[key] = decoder.decode(arr); + (["owner", "group", "type"] as ["owner", "group", "type"]).forEach( + (key): void => { + const arr = trim(header[key]); + if (arr.byteLength > 0) { + meta[key] = decoder.decode(arr); + } } - }); + ); - // read the file content - const len = parseInt(decoder.decode(header.fileSize), 8); - let rest = len; - while (rest > 0) { - await this.reader.readFull(this.block); - const arr = rest < recordSize ? this.block.subarray(0, rest) : this.block; - await Deno.copy(new Deno.Buffer(arr), writer); - rest -= recordSize; - } + meta.fileSize = parseInt(decoder.decode(header.fileSize), 8); + meta.type = types[meta.type as string] || meta.type; return meta; + }; + + async extract(): Promise<TarEntry | null> { + if (this.#entry && !this.#entry.consumed) { + // If entry body was not read, discard the body + // so we can read the next entry. + 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; + } } } |