From e4ca657f998603ef363f6549034d7beab8cb7eff Mon Sep 17 00:00:00 2001 From: nighca Date: Fri, 21 Nov 2025 12:30:04 +0800 Subject: [PATCH 1/2] Fix utf-8 encoding issue for unzip --- spx-gui/package-lock.json | 17 ++++++++++------- spx-gui/package.json | 2 +- spx-gui/src/utils/zip.ts | 32 ++++++++++++++++++++++++++------ 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/spx-gui/package-lock.json b/spx-gui/package-lock.json index 247553080..570e40ca3 100644 --- a/spx-gui/package-lock.json +++ b/spx-gui/package-lock.json @@ -11,6 +11,7 @@ "@jridgewell/resolve-uri": "^3.1.2", "@lottiefiles/dotlottie-vue": "^0.5.8", "@modelcontextprotocol/sdk": "^1.7.0", + "@nighca/fflate": "^0.8.3", "@rushstack/eslint-patch": "^1.7.2", "@scalar/api-reference": "^1.25.24", "@sentry/core": "^9.33.0", @@ -35,7 +36,6 @@ "dayjs": "^1.11.10", "eslint": "^8.56.0", "eslint-plugin-vue": "^9.20.1", - "fflate": "^0.8.2", "file-saver": "^2.0.5", "happy-dom": "^14.3.6", "hast": "^1.0.0", @@ -1777,6 +1777,15 @@ "node": ">=18" } }, + "node_modules/@nighca/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@nighca/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-XdSGXBiaSoboT2w81bXyjkzPEjHP3kkRvWaMmwYKT1N3m8Y8pJXV3n+JIEaVfeCpSDKV7npJD3lhvEim3+ZsNQ==", + "license": "MIT", + "engines": { + "node": "20.x" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5749,12 +5758,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", diff --git a/spx-gui/package.json b/spx-gui/package.json index 5b310e59d..31eea9472 100644 --- a/spx-gui/package.json +++ b/spx-gui/package.json @@ -17,6 +17,7 @@ "@jridgewell/resolve-uri": "^3.1.2", "@lottiefiles/dotlottie-vue": "^0.5.8", "@modelcontextprotocol/sdk": "^1.7.0", + "@nighca/fflate": "^0.8.3", "@rushstack/eslint-patch": "^1.7.2", "@scalar/api-reference": "^1.25.24", "@sentry/core": "^9.33.0", @@ -41,7 +42,6 @@ "dayjs": "^1.11.10", "eslint": "^8.56.0", "eslint-plugin-vue": "^9.20.1", - "fflate": "^0.8.2", "file-saver": "^2.0.5", "happy-dom": "^14.3.6", "hast": "^1.0.0", diff --git a/spx-gui/src/utils/zip.ts b/spx-gui/src/utils/zip.ts index d7e9dc586..4abbadb95 100644 --- a/spx-gui/src/utils/zip.ts +++ b/spx-gui/src/utils/zip.ts @@ -2,8 +2,8 @@ * @desc Zip-related utilities based on fflate */ -import { unzip as fflateUnzip, zip as fflateZip } from 'fflate' -import type * as fflate from 'fflate' +import { unzip as fflateUnzip, zip as fflateZip } from '@nighca/fflate' +import type * as fflate from '@nighca/fflate' export type Zippable = fflate.AsyncZippable export type ZipOptions = fflate.AsyncZipOptions & { @@ -32,12 +32,32 @@ export type UnzipOptions = fflate.AsyncUnzipOptions & { signal?: AbortSignal } +function decodeFilenameUtf8(bytes: Uint8Array): string { + return new TextDecoder('utf-8').decode(bytes) +} + export function unzip(data: Uint8Array, { signal, ...options }: UnzipOptions = {}) { return new Promise((resolve, reject) => { - const stopUnzipping = fflateUnzip(data, options ?? {}, (err, unzipped) => { - if (err) reject(err) - else resolve(unzipped) - }) + const stopUnzipping = fflateUnzip( + data, + { + // The ZIP spec says: + // > The ZIP format has historically supported only the original IBM PC character + // > encoding set, commonly referred to as IBM Code Page 437. + // > If general purpose bit 11 is unset, the file name and comment SHOULD conform + // > to the original ZIP character encoding. + // Now `fflate` defaults to Latin1 for that case, which is not what we want. + // For now popular zip tools use UTF-8 by default + // and some of them (e.g. macOS Archive Utility) **do not** set general purpose bit 11. + // So we just always decode as UTF-8 to provide maximum compatibility. + decodeFilename: decodeFilenameUtf8, + ...options + }, + (err, unzipped) => { + if (err) reject(err) + else resolve(unzipped) + } + ) signal?.addEventListener( 'abort', () => { From 5a8088d11940f31e0e5c3dcdd35b6cc5b5f874cc Mon Sep 17 00:00:00 2001 From: nighca Date: Fri, 21 Nov 2025 13:47:22 +0800 Subject: [PATCH 2/2] Use shared TextDecoder instance --- spx-gui/src/utils/zip.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spx-gui/src/utils/zip.ts b/spx-gui/src/utils/zip.ts index 4abbadb95..495e8bc6d 100644 --- a/spx-gui/src/utils/zip.ts +++ b/spx-gui/src/utils/zip.ts @@ -32,9 +32,7 @@ export type UnzipOptions = fflate.AsyncUnzipOptions & { signal?: AbortSignal } -function decodeFilenameUtf8(bytes: Uint8Array): string { - return new TextDecoder('utf-8').decode(bytes) -} +const utf8Decoder = new TextDecoder('utf-8') export function unzip(data: Uint8Array, { signal, ...options }: UnzipOptions = {}) { return new Promise((resolve, reject) => { @@ -50,7 +48,7 @@ export function unzip(data: Uint8Array, { signal, ...options }: UnzipOptions = { // For now popular zip tools use UTF-8 by default // and some of them (e.g. macOS Archive Utility) **do not** set general purpose bit 11. // So we just always decode as UTF-8 to provide maximum compatibility. - decodeFilename: decodeFilenameUtf8, + decodeFilename: (bytes) => utf8Decoder.decode(bytes), ...options }, (err, unzipped) => {