fix(stage-ui-live2d): ignore macOS archive metadata (#2405)

This commit is contained in:
Neko
2026-08-29 22:16:44 +08:00
committed by GitHub
parent 939bf544f8
commit 484a2b7b19
2 changed files with 35 additions and 0 deletions
@@ -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')
@@ -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) {