Files
NetMesh/lib/textZip.ts

111 lines
3.5 KiB
TypeScript
Raw Normal View History

export interface TextZipFile {
name: string;
content: string;
}
const ZIP_UTF8_FLAG = 0x0800;
const crcTable = Array.from({ length: 256 }, (_, index) => {
let value = index;
for (let bit = 0; bit < 8; bit += 1) {
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
}
return value >>> 0;
});
const crc32 = (bytes: Uint8Array): number => {
let crc = 0xffffffff;
bytes.forEach((byte) => {
crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
});
return (crc ^ 0xffffffff) >>> 0;
};
const pushUint16 = (parts: number[], value: number) => {
parts.push(value & 0xff, (value >>> 8) & 0xff);
};
const pushUint32 = (parts: number[], value: number) => {
parts.push(value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, (value >>> 24) & 0xff);
};
const concatZipParts = (header: number[], ...parts: Uint8Array[]): Uint8Array => {
const headerBytes = Uint8Array.from(header);
const chunk = new Uint8Array(
headerBytes.length + parts.reduce((total, part) => total + part.length, 0),
);
let offset = 0;
chunk.set(headerBytes, offset);
offset += headerBytes.length;
parts.forEach((part) => {
chunk.set(part, offset);
offset += part.length;
});
return chunk;
};
export const buildTextFilesZipBlob = (files: TextZipFile[]): Blob => {
const encoder = new TextEncoder();
const chunks: Uint8Array[] = [];
const centralDirectory: Uint8Array[] = [];
let offset = 0;
files.forEach((file) => {
const nameBytes = encoder.encode(file.name);
const contentBytes = encoder.encode(file.content);
const checksum = crc32(contentBytes);
const localHeader: number[] = [];
pushUint32(localHeader, 0x04034b50);
pushUint16(localHeader, 20);
pushUint16(localHeader, ZIP_UTF8_FLAG);
pushUint16(localHeader, 0);
pushUint16(localHeader, 0);
pushUint16(localHeader, 0);
pushUint32(localHeader, checksum);
pushUint32(localHeader, contentBytes.length);
pushUint32(localHeader, contentBytes.length);
pushUint16(localHeader, nameBytes.length);
pushUint16(localHeader, 0);
const localChunk = concatZipParts(localHeader, nameBytes, contentBytes);
chunks.push(localChunk);
const centralHeader: number[] = [];
pushUint32(centralHeader, 0x02014b50);
pushUint16(centralHeader, 20);
pushUint16(centralHeader, 20);
pushUint16(centralHeader, ZIP_UTF8_FLAG);
pushUint16(centralHeader, 0);
pushUint16(centralHeader, 0);
pushUint16(centralHeader, 0);
pushUint32(centralHeader, checksum);
pushUint32(centralHeader, contentBytes.length);
pushUint32(centralHeader, contentBytes.length);
pushUint16(centralHeader, nameBytes.length);
pushUint16(centralHeader, 0);
pushUint16(centralHeader, 0);
pushUint16(centralHeader, 0);
pushUint16(centralHeader, 0);
pushUint32(centralHeader, 0);
pushUint32(centralHeader, offset);
centralDirectory.push(concatZipParts(centralHeader, nameBytes));
offset += localChunk.length;
});
const centralDirectorySize = centralDirectory.reduce((total, chunk) => total + chunk.length, 0);
const endRecord: number[] = [];
pushUint32(endRecord, 0x06054b50);
pushUint16(endRecord, 0);
pushUint16(endRecord, 0);
pushUint16(endRecord, files.length);
pushUint16(endRecord, files.length);
pushUint32(endRecord, centralDirectorySize);
pushUint32(endRecord, offset);
pushUint16(endRecord, 0);
return new Blob([...chunks, ...centralDirectory, new Uint8Array(endRecord)], {
type: "application/zip",
});
};