mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix: keep non-ASCII names readable in zip downloads
The zip writer stores entry names as UTF-8 but did not set the UTF-8 flag (general purpose bit 11), so readers decoded them as CP437 or Latin-1. Any model, category or file name with a non-ASCII character came out garbled, including when the zip was imported back into Thingport.
This commit is contained in:
1 parent
407bce431a
commit
060be2f692
2 files changed
+42
-2
No files matched your search
@@ -4,6 +4,11 @@ import zlib from "node:zlib";
|
||||
|
||||
// Minimal streaming ZIP writer on node:zlib; yauzl is read-only.
|
||||
|
||||
// Bit 3: the CRC and sizes follow the data, in a descriptor. Bit 11: names are UTF-8. Without it readers
|
||||
// decode names as CP437 (yauzl, Python) or Latin-1 (fflate), garbling any non-ASCII model, category or
|
||||
// file name.
|
||||
const ENTRY_FLAGS = 0x0808;
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
@@ -70,7 +75,7 @@ export async function writeZip(destPath: string, entries: ZipEntryDescriptor[]):
|
||||
const localHeader = Buffer.alloc(30);
|
||||
localHeader.writeUInt32LE(0x04034b50, 0);
|
||||
localHeader.writeUInt16LE(20, 4); // version needed
|
||||
localHeader.writeUInt16LE(0x0008, 6); // flags: data descriptor follows
|
||||
localHeader.writeUInt16LE(ENTRY_FLAGS, 6); // flags
|
||||
localHeader.writeUInt16LE(8, 8); // method: deflate
|
||||
localHeader.writeUInt16LE(time, 10);
|
||||
localHeader.writeUInt16LE(date, 12);
|
||||
@@ -125,7 +130,7 @@ export async function writeZip(destPath: string, entries: ZipEntryDescriptor[]):
|
||||
central.writeUInt32LE(0x02014b50, 0);
|
||||
central.writeUInt16LE(20, 4); // version made by
|
||||
central.writeUInt16LE(20, 6); // version needed
|
||||
central.writeUInt16LE(0x0008, 8); // flags
|
||||
central.writeUInt16LE(ENTRY_FLAGS, 8); // flags
|
||||
central.writeUInt16LE(8, 10); // method
|
||||
central.writeUInt16LE(rec.time, 12);
|
||||
central.writeUInt16LE(rec.date, 14);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { writeZip } from "../src/utils/zipWriter";
|
||||
import { listZipEntries, readZipEntry } from "../src/utils/zipReader";
|
||||
|
||||
// Zip downloads name entries after models, categories and files, which are often not ASCII. A reader only
|
||||
// decodes a name as UTF-8 when the entry is flagged as such, so the round trip goes through the app's own
|
||||
// reader, the one behind zip imports and the consume folder.
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "thingport-zip-writer-"));
|
||||
|
||||
afterAll(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
describe("writeZip", () => {
|
||||
it("keeps non-ASCII names when the zip is read back", async () => {
|
||||
const source = path.join(dir, "benchy.stl");
|
||||
fs.writeFileSync(source, "solid benchy endsolid");
|
||||
const names = [
|
||||
"Gyvūnai/Šuniukas – Benchy/šuniukas.stl",
|
||||
"Unassigned/哨子/哨子.3mf",
|
||||
"Figurines/Pokémon Pikachu/supporting/notes.txt",
|
||||
"Tools/Cable clip/clip.stl",
|
||||
];
|
||||
const zipPath = path.join(dir, "names.zip");
|
||||
await writeZip(
|
||||
zipPath,
|
||||
names.map((arcname) => ({ arcname, filePath: source })),
|
||||
);
|
||||
|
||||
expect((await listZipEntries(zipPath)).map((e) => e.name)).toEqual(names);
|
||||
expect((await readZipEntry(zipPath, names[0], 1024))?.toString()).toBe("solid benchy endsolid");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user