diff --git a/src/handlers/index.ts b/src/handlers/index.ts index f5fdb663..36c92693 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -40,6 +40,7 @@ import textToShellHandler from "./texttoshell.ts"; import batchHandler from "./batch.ts"; import bsorHandler from "./bsor.ts"; import fontHandler from "./font.ts"; +import zstdHandler from "./zstd.ts"; const handlers: FormatHandler[] = []; try { handlers.push(new svgTraceHandler()) } catch (_) { }; @@ -85,5 +86,6 @@ try { handlers.push(new textToShellHandler()) } catch (_) { }; try { handlers.push(new batchHandler()) } catch (_) { }; try { handlers.push(new bsorHandler()) } catch (_) { }; try { handlers.push(new fontHandler()) } catch (_) { }; +try { handlers.push(new zstdHandler()) } catch (_) { }; export default handlers; diff --git a/src/handlers/zstd.ts b/src/handlers/zstd.ts new file mode 100644 index 00000000..8ac48c76 --- /dev/null +++ b/src/handlers/zstd.ts @@ -0,0 +1,54 @@ +import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts"; +import { ZSTDDecoder } from "three/examples/jsm/libs/zstddec.module.js"; + +const decoder = new ZSTDDecoder(); + +class zstdHandler implements FormatHandler { + public name = "zstd"; + + public supportedFormats: FileFormat[] = [ + { + name: "Zstandard Compressed Data", + format: "zst", + extension: "zst", + mime: "application/zstd", + from: true, + to: false, + internal: "zstd", + category: "archive", + lossless: true + }, + { + name: "Raw Binary Data", + format: "bin", + extension: "bin", + mime: "application/octet-stream", + from: false, + to: true, + internal: "raw", + category: "data", + lossless: true + } + ]; + + public ready = false; + + async init() { + await decoder.init(); + this.ready = true; + } + + async doConvert(inputFiles: FileData[], inputFormat: FileFormat, outputFormat: FileFormat): Promise { + if (inputFormat.internal === "zstd" && outputFormat.internal === "raw") { + return inputFiles.map((inputFile) => { + const bytes = decoder.decode(inputFile.bytes); + const name = inputFile.name.replace(/\.(zst|zstd)$/i, "") || `${inputFile.name}.bin`; + return { name, bytes }; + }); + } + + throw "Invalid conversion path."; + } +} + +export default zstdHandler; diff --git a/src/normalizeMimeType.ts b/src/normalizeMimeType.ts index c58dc5ec..d091bb0e 100644 --- a/src/normalizeMimeType.ts +++ b/src/normalizeMimeType.ts @@ -3,6 +3,8 @@ function normalizeMimeType (mime: string) { case "audio/x-wav": return "audio/wav"; case "audio/vnd.wave": return "audio/wav"; case "application/x-gzip": return "application/gzip"; + case "application/x-zstd": return "application/zstd"; + case "application/zst": return "application/zstd"; case "image/x-icon": return "image/vnd.microsoft.icon"; case "image/vtf": return "image/x-vtf"; case "image/qoi": return "image/x-qoi"; diff --git a/test/zstd.test.ts b/test/zstd.test.ts new file mode 100644 index 00000000..6c53c3ff --- /dev/null +++ b/test/zstd.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test"; +import zstdHandler from "../src/handlers/zstd.ts"; +import type { FileFormat } from "../src/FormatHandler.ts"; + +const zstdFormat: FileFormat = { + name: "Zstandard Compressed Data", + format: "zst", + extension: "zst", + mime: "application/zstd", + from: true, + to: false, + internal: "zstd" +}; + +const rawFormat: FileFormat = { + name: "Raw Binary Data", + format: "bin", + extension: "bin", + mime: "application/octet-stream", + from: false, + to: true, + internal: "raw" +}; + +test("zstd handler decodes .zst to raw bytes", async () => { + const input = new TextEncoder().encode("hello zstd"); + const compressed = Bun.zstdCompressSync(input); + + const handler = new zstdHandler(); + await handler.init(); + + const output = await handler.doConvert( + [{ name: "sample.zst", bytes: new Uint8Array(compressed) }], + zstdFormat, + rawFormat + ); + + expect(output).toHaveLength(1); + expect(output[0].name).toBe("sample"); + expect(new TextDecoder().decode(output[0].bytes)).toBe("hello zstd"); +});