import { FileTypes, type TarInfo, type TarMeta, type TarOptions, ustarStructure,} from "./_common.ts";import type { Reader } from "../types.d.ts";
export { type TarInfo, type TarMeta, type TarOptions };
import { MultiReader } from "../io/multi_reader.ts";import { Buffer } from "../io/buffer.ts";import { assert } from "../_util/asserts.ts";import { recordSize } from "./_common.ts";
const ustar = "ustar\u000000";
class FileReader implements Reader { #file?: Deno.FsFile;
constructor(private filePath: string) {}
public async read(p: Uint8Array): Promise<number | null> { if (!this.#file) { this.#file = await Deno.open(this.filePath, { read: true }); } const res = await this.#file.read(p); if (res === null) { this.#file.close(); this.#file = undefined; } return res; }}
function clean(length: number): Uint8Array { const buffer = new Uint8Array(length); buffer.fill(0, 0, length - 1); return buffer;}
function pad(num: number, bytes: number, base = 8): string { const numString = num.toString(base); return "000000000000".slice(numString.length + 12 - bytes) + numString;}
function formatHeader(data: TarData): Uint8Array { const encoder = new TextEncoder(), buffer = clean(512); let offset = 0; ustarStructure.forEach(function (value) { const entry = encoder.encode(data[value.field as keyof TarData] || ""); buffer.set(entry, offset); offset += value.length; }); return buffer;}
export interface TarData { fileName?: string; fileNamePrefix?: string; fileMode?: string; uid?: string; gid?: string; fileSize?: string; mtime?: string; checksum?: string; type?: string; ustar?: string; owner?: string; group?: string;}
export interface TarDataWithSource extends TarData { filePath?: string; reader?: Reader;}
export class Tar { data: TarDataWithSource[];
constructor() { this.data = []; }
async append(fn: string, opts: TarOptions) { if (typeof fn !== "string") { throw new Error("file name not specified"); } let fileName = fn; let fileNamePrefix: string | undefined; if (fileName.length > 100) { let i = fileName.length; while (i >= 0) { i = fileName.lastIndexOf("/", i); if (i <= 155) { fileNamePrefix = fileName.slice(0, i); fileName = fileName.slice(i + 1); break; } i--; } const errMsg = "ustar format does not allow a long file name (length of [file name" + "prefix] + / + [file name] must be shorter than 256 bytes)"; if (i < 0 || fileName.length > 100) { throw new Error(errMsg); } else { assert(fileNamePrefix != null); if (fileNamePrefix.length > 155) { throw new Error(errMsg); } } }
opts = opts || {};
let info: Deno.FileInfo | undefined; if (opts.filePath) { info = await Deno.stat(opts.filePath); if (info.isDirectory) { info.size = 0; opts.reader = new Buffer(); } }
const mode = opts.fileMode || (info && info.mode) || parseInt("777", 8) & 0xfff, mtime = Math.floor( opts.mtime ?? (info?.mtime ?? new Date()).valueOf() / 1000, ), uid = opts.uid || 0, gid = opts.gid || 0; if (typeof opts.owner === "string" && opts.owner.length >= 32) { throw new Error( "ustar format does not allow owner name length >= 32 bytes", ); } if (typeof opts.group === "string" && opts.group.length >= 32) { throw new Error( "ustar format does not allow group name length >= 32 bytes", ); }
const fileSize = info?.size ?? opts.contentSize; assert(fileSize != null, "fileSize must be set");
const type = opts.type ? FileTypes[opts.type as keyof typeof FileTypes] : (info?.isDirectory ? FileTypes.directory : FileTypes.file); const tarData: TarDataWithSource = { fileName, fileNamePrefix, fileMode: pad(mode, 7), uid: pad(uid, 7), gid: pad(gid, 7), fileSize: pad(fileSize, 11), mtime: pad(mtime, 11), checksum: " ", type: type.toString(), ustar, owner: opts.owner || "", group: opts.group || "", filePath: opts.filePath, reader: opts.reader, };
let checksum = 0; const encoder = new TextEncoder(); Object.keys(tarData) .filter((key): boolean => ["filePath", "reader"].indexOf(key) < 0) .forEach(function (key) { checksum += encoder .encode(tarData[key as keyof TarData]) .reduce((p, c): number => p + c, 0); });
tarData.checksum = pad(checksum, 6) + "\u0000 "; this.data.push(tarData); }
getReader(): Reader { const readers: Reader[] = []; this.data.forEach((tarData) => { let { reader } = tarData; const { filePath } = tarData; const headerArr = formatHeader(tarData); readers.push(new Buffer(headerArr)); if (!reader) { assert(filePath != null); reader = new FileReader(filePath); } readers.push(reader);
assert(tarData.fileSize != null, "fileSize must be set"); readers.push( new Buffer( clean( recordSize - (parseInt(tarData.fileSize, 8) % recordSize || recordSize), ), ), ); });
readers.push(new Buffer(clean(recordSize * 2))); return new MultiReader(readers); }}