fix(stage-ui-live2d): normalize Live2D model file references (#2047)

Authored-by-agent: Codex <codex@openai.com>
This commit is contained in:
Neko
2026-07-11 23:44:41 +08:00
committed by GitHub
parent 369208afbe
commit ed664e8107
2 changed files with 112 additions and 1 deletions
@@ -47,6 +47,19 @@ function createShisihangshiSettingsText(): string {
})
}
function createNonAsciiSettingsText(): string {
return JSON.stringify({
Version: 3,
FileReferences: {
Moc: '模型文件.moc3',
Textures: ['模型贴图.4096/texture_00.png'],
Physics: '模型文件.physics3.json',
DisplayInfo: '模型文件.cdi3.json',
},
Groups: [],
})
}
const appleDoubleHeader = new Uint8Array([0, 5, 22, 7, 0, 2, 0, 0, 77, 97, 99, 32, 79, 83, 32, 88])
describe('live2d zip loader settings sanitization', () => {
@@ -137,6 +150,89 @@ describe('live2d zip loader settings sanitization', () => {
expect(() => settings.validateFiles(files.map(file => encodeURI(file.webkitRelativePath)))).not.toThrow()
})
it('loads an OPFS-restored file directory when model3.json references non-ASCII file names', async () => {
await import('./live2d-zip-loader')
const { FileLoader } = await import('pixi-live2d-display/cubism4')
const files = [
fileWithRelativePath(
createNonAsciiSettingsText(),
'模型文件.model3.json',
'非ASCII模型26045/模型文件.model3.json',
),
fileWithRelativePath(
new Uint8Array([77, 79, 67, 51]),
'模型文件.moc3',
'非ASCII模型26045/模型文件.moc3',
),
fileWithRelativePath(
new Uint8Array([1, 2, 3]),
'texture_00.png',
'非ASCII模型26045/模型贴图.4096/texture_00.png',
),
fileWithRelativePath(
'{}',
'模型文件.physics3.json',
'非ASCII模型26045/模型文件.physics3.json',
),
fileWithRelativePath(
'{}',
'模型文件.cdi3.json',
'非ASCII模型26045/模型文件.cdi3.json',
),
]
const settings = await FileLoader.createSettings(files)
// ROOT CAUSE:
//
// pixi-live2d-display validates OPFS-restored File objects against
// encodeURI(file.webkitRelativePath), but the settings created from the
// original model3.json currently keep non-ASCII file references unencoded.
//
// Before the fix, the settings expect "模型文件.moc3" while the available
// file list contains URI-encoded non-ASCII directory paths, so validation
// reports that the moc3 file does not exist.
expect(() => settings.validateFiles(files.map(file => encodeURI(file.webkitRelativePath)))).not.toThrow()
})
it('does not double encode existing URI-encoded model3.json file references', async () => {
await import('./live2d-zip-loader')
const { FileLoader } = await import('pixi-live2d-display/cubism4')
const files = [
fileWithRelativePath(
JSON.stringify({
Version: 3,
FileReferences: {
Moc: '%E6%A8%A1%E5%9E%8B%E6%96%87%E4%BB%B6.moc3',
Textures: ['%E6%A8%A1%E5%9E%8B%E8%B4%B4%E5%9B%BE.4096/texture_00.png'],
},
Groups: [],
}),
'模型文件.model3.json',
'非ASCII模型26045/模型文件.model3.json',
),
fileWithRelativePath(
new Uint8Array([77, 79, 67, 51]),
'模型文件.moc3',
'非ASCII模型26045/模型文件.moc3',
),
fileWithRelativePath(
new Uint8Array([1, 2, 3]),
'texture_00.png',
'非ASCII模型26045/模型贴图.4096/texture_00.png',
),
]
const settings = await FileLoader.createSettings(files)
expect(settings.moc).toBe('%E6%A8%A1%E5%9E%8B%E6%96%87%E4%BB%B6.moc3')
expect(settings.textures).toEqual(['%E6%A8%A1%E5%9E%8B%E8%B4%B4%E5%9B%BE.4096/texture_00.png'])
expect(settings.moc).not.toContain('%25E6')
expect(() => settings.validateFiles(files.map(file => encodeURI(file.webkitRelativePath)))).not.toThrow()
})
it('loads an OPFS-restored file directory when a macOS AppleDouble settings sidecar is present before the real settings file', async () => {
await import('./live2d-zip-loader')
const { FileLoader } = await import('pixi-live2d-display/cubism4')
@@ -109,7 +109,22 @@ function createModelSettings(text: string, url: string): ModelSettings {
throw new Error('Unknown settings JSON')
}
return runtime.createModelSettings(settingsJSON)
// pixi-live2d-display validates files with encodeURI(file.webkitRelativePath).
// Decode first so archives that already store URI-encoded references do not
// get `%` encoded again.
const settings = runtime.createModelSettings(settingsJSON)
const normalizeFileReference = (file: string) => {
try {
return encodeURI(decodeURI(file))
}
catch {
return encodeURI(file)
}
}
settings.url = normalizeFileReference(settings.url)
settings.replaceFiles(normalizeFileReference)
return settings
}
export function isSettingsFile(file: string) {