mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-18 03:30:39 +09:00
Fix portability import and org chart test blockers
This commit is contained in:
parent
1830216078
commit
553e7b6b30
5 changed files with 251 additions and 12 deletions
|
|
@ -1,5 +1,6 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import { deflateRawSync } from "node:zlib";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createZipArchive, readZipArchive } from "./zip";
|
||||
|
||||
|
|
@ -20,6 +21,95 @@ function readString(bytes: Uint8Array, offset: number, length: number) {
|
|||
return new TextDecoder().decode(bytes.slice(offset, offset + length));
|
||||
}
|
||||
|
||||
function writeUint16(target: Uint8Array, offset: number, value: number) {
|
||||
target[offset] = value & 0xff;
|
||||
target[offset + 1] = (value >>> 8) & 0xff;
|
||||
}
|
||||
|
||||
function writeUint32(target: Uint8Array, offset: number, value: number) {
|
||||
target[offset] = value & 0xff;
|
||||
target[offset + 1] = (value >>> 8) & 0xff;
|
||||
target[offset + 2] = (value >>> 16) & 0xff;
|
||||
target[offset + 3] = (value >>> 24) & 0xff;
|
||||
}
|
||||
|
||||
function crc32(bytes: Uint8Array) {
|
||||
let crc = 0xffffffff;
|
||||
for (const byte of bytes) {
|
||||
crc ^= byte;
|
||||
for (let bit = 0; bit < 8; bit += 1) {
|
||||
crc = (crc & 1) === 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
||||
}
|
||||
}
|
||||
return (crc ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function createDeflatedZipArchive(files: Record<string, string>, rootPath: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const localChunks: Uint8Array[] = [];
|
||||
const centralChunks: Uint8Array[] = [];
|
||||
let localOffset = 0;
|
||||
let entryCount = 0;
|
||||
|
||||
for (const [relativePath, content] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
const fileName = encoder.encode(`${rootPath}/${relativePath}`);
|
||||
const rawBody = encoder.encode(content);
|
||||
const deflatedBody = new Uint8Array(deflateRawSync(rawBody));
|
||||
const checksum = crc32(rawBody);
|
||||
|
||||
const localHeader = new Uint8Array(30 + fileName.length);
|
||||
writeUint32(localHeader, 0, 0x04034b50);
|
||||
writeUint16(localHeader, 4, 20);
|
||||
writeUint16(localHeader, 6, 0x0800);
|
||||
writeUint16(localHeader, 8, 8);
|
||||
writeUint32(localHeader, 14, checksum);
|
||||
writeUint32(localHeader, 18, deflatedBody.length);
|
||||
writeUint32(localHeader, 22, rawBody.length);
|
||||
writeUint16(localHeader, 26, fileName.length);
|
||||
localHeader.set(fileName, 30);
|
||||
|
||||
const centralHeader = new Uint8Array(46 + fileName.length);
|
||||
writeUint32(centralHeader, 0, 0x02014b50);
|
||||
writeUint16(centralHeader, 4, 20);
|
||||
writeUint16(centralHeader, 6, 20);
|
||||
writeUint16(centralHeader, 8, 0x0800);
|
||||
writeUint16(centralHeader, 10, 8);
|
||||
writeUint32(centralHeader, 16, checksum);
|
||||
writeUint32(centralHeader, 20, deflatedBody.length);
|
||||
writeUint32(centralHeader, 24, rawBody.length);
|
||||
writeUint16(centralHeader, 28, fileName.length);
|
||||
writeUint32(centralHeader, 42, localOffset);
|
||||
centralHeader.set(fileName, 46);
|
||||
|
||||
localChunks.push(localHeader, deflatedBody);
|
||||
centralChunks.push(centralHeader);
|
||||
localOffset += localHeader.length + deflatedBody.length;
|
||||
entryCount += 1;
|
||||
}
|
||||
|
||||
const centralDirectoryLength = centralChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const archive = new Uint8Array(
|
||||
localChunks.reduce((sum, chunk) => sum + chunk.length, 0) + centralDirectoryLength + 22,
|
||||
);
|
||||
let offset = 0;
|
||||
for (const chunk of localChunks) {
|
||||
archive.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
const centralDirectoryOffset = offset;
|
||||
for (const chunk of centralChunks) {
|
||||
archive.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
writeUint32(archive, offset, 0x06054b50);
|
||||
writeUint16(archive, offset + 8, entryCount);
|
||||
writeUint16(archive, offset + 10, entryCount);
|
||||
writeUint32(archive, offset + 12, centralDirectoryLength);
|
||||
writeUint32(archive, offset + 16, centralDirectoryOffset);
|
||||
|
||||
return archive;
|
||||
}
|
||||
|
||||
describe("createZipArchive", () => {
|
||||
it("writes a zip archive with the export root path prefixed into each entry", () => {
|
||||
const archive = createZipArchive(
|
||||
|
|
@ -51,7 +141,7 @@ describe("createZipArchive", () => {
|
|||
expect(readUint16(archive, endOffset + 10)).toBe(2);
|
||||
});
|
||||
|
||||
it("reads a Paperclip zip archive back into rootPath and file contents", () => {
|
||||
it("reads a Paperclip zip archive back into rootPath and file contents", async () => {
|
||||
const archive = createZipArchive(
|
||||
{
|
||||
"COMPANY.md": "# Company\n",
|
||||
|
|
@ -61,7 +151,7 @@ describe("createZipArchive", () => {
|
|||
"paperclip-demo",
|
||||
);
|
||||
|
||||
expect(readZipArchive(archive)).toEqual({
|
||||
await expect(readZipArchive(archive)).resolves.toEqual({
|
||||
rootPath: "paperclip-demo",
|
||||
files: {
|
||||
"COMPANY.md": "# Company\n",
|
||||
|
|
@ -71,7 +161,7 @@ describe("createZipArchive", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("round-trips binary image files without coercing them to text", () => {
|
||||
it("round-trips binary image files without coercing them to text", async () => {
|
||||
const archive = createZipArchive(
|
||||
{
|
||||
"images/company-logo.png": {
|
||||
|
|
@ -83,7 +173,7 @@ describe("createZipArchive", () => {
|
|||
"paperclip-demo",
|
||||
);
|
||||
|
||||
expect(readZipArchive(archive)).toEqual({
|
||||
await expect(readZipArchive(archive)).resolves.toEqual({
|
||||
rootPath: "paperclip-demo",
|
||||
files: {
|
||||
"images/company-logo.png": {
|
||||
|
|
@ -94,4 +184,22 @@ describe("createZipArchive", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reads standard DEFLATE zip archives created outside Paperclip", async () => {
|
||||
const archive = createDeflatedZipArchive(
|
||||
{
|
||||
"COMPANY.md": "# Company\n",
|
||||
"agents/ceo/AGENTS.md": "# CEO\n",
|
||||
},
|
||||
"paperclip-demo",
|
||||
);
|
||||
|
||||
await expect(readZipArchive(archive)).resolves.toEqual({
|
||||
rootPath: "paperclip-demo",
|
||||
files: {
|
||||
"COMPANY.md": "# Company\n",
|
||||
"agents/ceo/AGENTS.md": "# CEO\n",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue