From 484a2b7b19e7dbdd099189d6626099770dff0e06 Mon Sep 17 00:00:00 2001 From: Neko Date: Sat, 29 Aug 2026 22:16:44 +0800 Subject: [PATCH] fix(stage-ui-live2d): ignore macOS archive metadata (#2405) --- .../src/utils/live2d-zip-loader.test.ts | 34 +++++++++++++++++++ .../src/utils/live2d-zip-loader.ts | 1 + 2 files changed, 35 insertions(+) diff --git a/packages/stage-ui-live2d/src/utils/live2d-zip-loader.test.ts b/packages/stage-ui-live2d/src/utils/live2d-zip-loader.test.ts index c8cb57721..dcfad0aef 100644 --- a/packages/stage-ui-live2d/src/utils/live2d-zip-loader.test.ts +++ b/packages/stage-ui-live2d/src/utils/live2d-zip-loader.test.ts @@ -207,6 +207,40 @@ describe('live2d zip loader settings sanitization', () => { expect(filePaths).not.toContain('__MACOSX/302301_shisihangshi/._302301_shisihangshi.model3.json') }) + it('ignores macOS AppleDouble expression sidecars during zip metadata extraction', async () => { + await import('./live2d-zip-loader') + const { ZipLoader } = await import('pixi-live2d-display/cubism4') + + const zip = new JSZip() + zip.file('302301_shisihangshi/302301_shisihangshi.model3.json', createShisihangshiSettingsText()) + zip.file('302301_shisihangshi/expressions/happy.exp3.json', JSON.stringify({ + Type: 'Live2D Expression', + Parameters: [{ Id: 'ParamEyeLOpen', Value: 1, Blend: 'Add' }], + })) + zip.file('__MACOSX/302301_shisihangshi/expressions/._happy.exp3.json', appleDoubleHeader) + + const zipBytes = await zip.generateAsync({ type: 'uint8array' }) + const reader = await JSZip.loadAsync(await blobFromBytes(zipBytes).arrayBuffer()) + const settings = await ZipLoader.createSettings(reader) + + // ROOT CAUSE: + // + // Metadata extraction scanned every ZIP entry. It parsed the AppleDouble sidecar as JSON, + // then discarded the valid expression metadata when that parse failed. + // + // The loader now removes macOS metadata paths before it discovers model resources. + expect(settings).toHaveProperty('_expFiles', [ + { + name: 'happy', + fileName: '302301_shisihangshi/expressions/happy.exp3.json', + data: { + Type: 'Live2D Expression', + Parameters: [{ Id: 'ParamEyeLOpen', Value: 1, Blend: 'Add' }], + }, + }, + ]) + }) + it('loads an OPFS-restored file directory when model3.json contains Physics: null', async () => { await import('./live2d-zip-loader') const { FileLoader } = await import('pixi-live2d-display/cubism4') diff --git a/packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts b/packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts index 536daf965..7cafc939a 100644 --- a/packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts +++ b/packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts @@ -29,6 +29,7 @@ function shouldIgnoreLive2DArchiveEntry(filePath: string): boolean { ZipLoader.createSettings = async (reader: JSZip) => { const filePaths = Object.keys(reader.files) + .filter(filePath => !shouldIgnoreLive2DArchiveEntry(filePath)) const settings = await (async () => { const settingsFilePath = filePaths.find(file => isSettingsFile(file)) if (!settingsFilePath) {