make some overall QoL improvements
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/** Portable workspace archive: a gzip-compressed ustar file with JSON and images. */
|
||||
import {gzipSync, gunzipSync} from "node:zlib";
|
||||
|
||||
export const MAX_ARCHIVE_BYTES = 250 * 1024 * 1024;
|
||||
|
||||
function header(name, size) {
|
||||
const block = Buffer.alloc(512);
|
||||
block.write(name, 0, 100, "ascii");
|
||||
block.write("0000644\0", 100, "ascii");
|
||||
block.write("0000000\0", 108, "ascii");
|
||||
block.write("0000000\0", 116, "ascii");
|
||||
block.write(size.toString(8).padStart(11, "0") + "\0", 124, "ascii");
|
||||
block.write(Math.floor(Date.now() / 1000).toString(8).padStart(11, "0") + "\0", 136, "ascii");
|
||||
block.fill(32, 148, 156);
|
||||
block.write("0", 156, "ascii");
|
||||
block.write("ustar\0", 257, "ascii");
|
||||
block.write("00", 263, "ascii");
|
||||
const checksum = block.reduce((sum, byte) => sum + byte, 0);
|
||||
block.write(checksum.toString(8).padStart(6, "0") + "\0 ", 148, "ascii");
|
||||
return block;
|
||||
}
|
||||
|
||||
export function createWorkspaceArchive(workspace, images) {
|
||||
const files = [["workspace.json", Buffer.from(JSON.stringify(workspace, null, 2))], ...images];
|
||||
const chunks = [];
|
||||
let total = 1024;
|
||||
for (const [name, body] of files) {
|
||||
if (!/^(workspace\.json|images\/[a-f0-9-]{36}\.(png|jpeg|webp))$/.test(name)) throw new Error("Invalid archive entry");
|
||||
total += 512 + Math.ceil(body.length / 512) * 512;
|
||||
if (total > MAX_ARCHIVE_BYTES) throw new Error("Workspace archive exceeds 250 MB");
|
||||
chunks.push(header(name, body.length), body, Buffer.alloc((512 - body.length % 512) % 512));
|
||||
}
|
||||
chunks.push(Buffer.alloc(1024));
|
||||
return gzipSync(Buffer.concat(chunks, total), {level: 1});
|
||||
}
|
||||
|
||||
export function parseWorkspaceArchive(buffer) {
|
||||
if (!Buffer.isBuffer(buffer) || buffer.length > MAX_ARCHIVE_BYTES) throw new Error("Archive exceeds 250 MB");
|
||||
const tar = gunzipSync(buffer, {maxOutputLength: MAX_ARCHIVE_BYTES});
|
||||
const files = new Map();
|
||||
for (let offset = 0; offset + 512 <= tar.length;) {
|
||||
const block = tar.subarray(offset, offset + 512);
|
||||
if (block.every(byte => byte === 0)) break;
|
||||
const name = block.toString("ascii", 0, 100).split("\0")[0];
|
||||
if (!/^(workspace\.json|images\/[a-f0-9-]{36}\.(png|jpeg|webp))$/.test(name) || files.has(name) || block.toString("ascii", 156, 157) !== "0") {
|
||||
throw new Error("Invalid archive entry");
|
||||
}
|
||||
const recorded = Number.parseInt(block.toString("ascii", 148, 156).trim(), 8);
|
||||
const copy = Buffer.from(block); copy.fill(32, 148, 156);
|
||||
if (recorded !== copy.reduce((sum, byte) => sum + byte, 0)) throw new Error("Invalid archive checksum");
|
||||
const size = Number.parseInt(block.toString("ascii", 124, 136).replaceAll("\0", "").trim(), 8);
|
||||
if (!Number.isSafeInteger(size) || size < 0 || offset + 512 + size > tar.length) throw new Error("Invalid archive size");
|
||||
files.set(name, tar.subarray(offset + 512, offset + 512 + size));
|
||||
offset += 512 + Math.ceil(size / 512) * 512;
|
||||
}
|
||||
if (!files.has("workspace.json")) throw new Error("Archive has no workspace.json");
|
||||
let workspace;
|
||||
try { workspace = JSON.parse(files.get("workspace.json").toString("utf8")); }
|
||||
catch { throw new Error("Archive has invalid workspace.json"); }
|
||||
return {workspace, images: files};
|
||||
}
|
||||
Reference in New Issue
Block a user