Initial commit
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import { gzipSync, deflateRawSync } from "node:zlib";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
/** Build a single ustar tar header block (512 bytes) for a regular file. mtime is
|
||||
* fixed at 0 so the archive is deterministic for the same inputs. */
|
||||
function tarHeader(name: string, size: number): Buffer {
|
||||
let prefix = "";
|
||||
let nm = name;
|
||||
if (Buffer.byteLength(name) > 100) {
|
||||
let split = -1;
|
||||
for (let p = 0; p < name.length; p++) {
|
||||
if (name[p] === "/" && Buffer.byteLength(name.slice(p + 1)) <= 100 && Buffer.byteLength(name.slice(0, p)) <= 155) {
|
||||
split = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (split < 0) throw new Error("path too long for tar: " + name);
|
||||
prefix = name.slice(0, split);
|
||||
nm = name.slice(split + 1);
|
||||
}
|
||||
const h = Buffer.alloc(512);
|
||||
h.write(nm, 0, 100, "utf8");
|
||||
h.write("0000644\0", 100, 8, "ascii"); // mode
|
||||
h.write("0000000\0", 108, 8, "ascii"); // uid
|
||||
h.write("0000000\0", 116, 8, "ascii"); // gid
|
||||
h.write(size.toString(8).padStart(11, "0") + "\0", 124, 12, "ascii"); // size (octal)
|
||||
h.write("00000000000\0", 136, 12, "ascii"); // mtime = 0
|
||||
h.write(" ", 148, 8, "ascii"); // checksum placeholder (8 spaces)
|
||||
h.write("0", 156, 1, "ascii"); // typeflag: regular file
|
||||
h.write("ustar\0", 257, 6, "ascii");
|
||||
h.write("00", 263, 2, "ascii");
|
||||
if (prefix) h.write(prefix, 345, 155, "utf8");
|
||||
let sum = 0;
|
||||
for (let i = 0; i < 512; i++) sum += h[i]!;
|
||||
h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8, "ascii");
|
||||
return h;
|
||||
}
|
||||
|
||||
/** Pack files into a deterministic .tar.gz (sorted by path, zero mtime). */
|
||||
export function makeTarGz(files: Array<{ path: string; bytes: Buffer }>): Buffer {
|
||||
const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
||||
const parts: Buffer[] = [];
|
||||
for (const f of sorted) {
|
||||
parts.push(tarHeader(f.path, f.bytes.length));
|
||||
parts.push(f.bytes);
|
||||
const pad = (512 - (f.bytes.length % 512)) % 512;
|
||||
if (pad) parts.push(Buffer.alloc(pad));
|
||||
}
|
||||
parts.push(Buffer.alloc(1024)); // two zero blocks = end of archive
|
||||
return gzipSync(Buffer.concat(parts), { level: 9 });
|
||||
}
|
||||
|
||||
export function sha256hex(buf: Buffer): string {
|
||||
return createHash("sha256").update(buf).digest("hex");
|
||||
}
|
||||
|
||||
// ---- ZIP (deflate) ----
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
t[n] = c >>> 0;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
|
||||
function crc32(buf: Buffer): number {
|
||||
let c = 0xffffffff;
|
||||
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]!) & 0xff]! ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
/** Pack files into a deterministic .zip (deflate; fixed DOS timestamp). */
|
||||
export function makeZip(files: Array<{ path: string; bytes: Buffer }>): Buffer {
|
||||
const sorted = [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
||||
const DOS_TIME = 0; // fixed → deterministic
|
||||
const DOS_DATE = 0x21; // 1980-01-01
|
||||
const local: Buffer[] = [];
|
||||
const central: Buffer[] = [];
|
||||
let offset = 0;
|
||||
for (const f of sorted) {
|
||||
const name = Buffer.from(f.path, "utf8");
|
||||
const crc = crc32(f.bytes);
|
||||
const comp = deflateRawSync(f.bytes, { level: 9 });
|
||||
const lh = Buffer.alloc(30);
|
||||
lh.writeUInt32LE(0x04034b50, 0);
|
||||
lh.writeUInt16LE(20, 4); // version needed
|
||||
lh.writeUInt16LE(0, 6); // flags
|
||||
lh.writeUInt16LE(8, 8); // method: deflate
|
||||
lh.writeUInt16LE(DOS_TIME, 10);
|
||||
lh.writeUInt16LE(DOS_DATE, 12);
|
||||
lh.writeUInt32LE(crc, 14);
|
||||
lh.writeUInt32LE(comp.length, 18);
|
||||
lh.writeUInt32LE(f.bytes.length, 22);
|
||||
lh.writeUInt16LE(name.length, 26);
|
||||
lh.writeUInt16LE(0, 28);
|
||||
local.push(lh, name, comp);
|
||||
|
||||
const ch = Buffer.alloc(46);
|
||||
ch.writeUInt32LE(0x02014b50, 0);
|
||||
ch.writeUInt16LE(20, 4); // version made by
|
||||
ch.writeUInt16LE(20, 6); // version needed
|
||||
ch.writeUInt16LE(0, 8); // flags
|
||||
ch.writeUInt16LE(8, 10); // method
|
||||
ch.writeUInt16LE(DOS_TIME, 12);
|
||||
ch.writeUInt16LE(DOS_DATE, 14);
|
||||
ch.writeUInt32LE(crc, 16);
|
||||
ch.writeUInt32LE(comp.length, 20);
|
||||
ch.writeUInt32LE(f.bytes.length, 24);
|
||||
ch.writeUInt16LE(name.length, 28);
|
||||
ch.writeUInt16LE(0, 30); // extra len
|
||||
ch.writeUInt16LE(0, 32); // comment len
|
||||
ch.writeUInt16LE(0, 34); // disk number
|
||||
ch.writeUInt16LE(0, 36); // internal attrs
|
||||
ch.writeUInt32LE(0, 38); // external attrs
|
||||
ch.writeUInt32LE(offset, 42);
|
||||
central.push(ch, name);
|
||||
|
||||
offset += lh.length + name.length + comp.length;
|
||||
}
|
||||
const centralBuf = Buffer.concat(central);
|
||||
const localBuf = Buffer.concat(local);
|
||||
const eocd = Buffer.alloc(22);
|
||||
eocd.writeUInt32LE(0x06054b50, 0);
|
||||
eocd.writeUInt16LE(0, 4);
|
||||
eocd.writeUInt16LE(0, 6);
|
||||
eocd.writeUInt16LE(sorted.length, 8);
|
||||
eocd.writeUInt16LE(sorted.length, 10);
|
||||
eocd.writeUInt32LE(centralBuf.length, 12);
|
||||
eocd.writeUInt32LE(localBuf.length, 16);
|
||||
eocd.writeUInt16LE(0, 20);
|
||||
return Buffer.concat([localBuf, centralBuf, eocd]);
|
||||
}
|
||||
Reference in New Issue
Block a user