'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const utils = require('../js/spine-utils'); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'spine-security-')); try { for (const unsafe of [ '../../../secret.png', '..\\..\\secret.png', '/tmp/secret.png', 'C:\\secret.png', 'C:secret.png', '\\\\server\\share\\secret.png', 'file:///tmp/secret.png', 'folder/../secret.png', 'folder\\..\\secret.png', 'bad\0name.png', ]) { assert.throws(() => utils.validateAtlasPageName(unsafe), (error) => error && error.code === 'UNSAFE_ATLAS_PATH', unsafe); } assert.equal(utils.validateAtlasPageName('./textures/page.png'), 'textures/page.png'); const atlasDir = path.join(root, 'atlas'); fs.mkdirSync(path.join(atlasDir, 'textures'), { recursive: true }); const nestedTexture = path.join(atlasDir, 'textures', 'page.png'); fs.writeFileSync(nestedTexture, 'texture'); assert.equal(utils.resolveAtlasTextureFile(atlasDir, 'textures/page.png'), nestedTexture); const outside = path.join(root, 'outside.png'); const link = path.join(atlasDir, 'linked.png'); fs.writeFileSync(outside, 'private'); try { fs.symlinkSync(outside, link); assert.throws(() => utils.resolveAtlasTextureFile(atlasDir, 'linked.png'), (error) => error && error.code === 'UNSAFE_ATLAS_PATH'); } catch (error) { if (!['EPERM', 'EACCES'].includes(error.code)) throw error; } const library = path.join(root, 'safe.library'); const images = path.join(library, 'images'); fs.mkdirSync(images, { recursive: true }); fs.writeFileSync(path.join(library, 'metadata.json'), JSON.stringify({ folders: [{ id: 'spine', name: 'Spine', children: [] }] })); function item(id, name, contents) { const dir = path.join(images, id + '.info'); fs.mkdirSync(dir); fs.writeFileSync(path.join(dir, 'metadata.json'), JSON.stringify({ id, name: path.parse(name).name, ext: path.extname(name).slice(1), folders: ['spine'], isDeleted: false })); const file = path.join(dir, name); fs.writeFileSync(file, contents); return file; } const skeleton = item('SKELETON', 'hero.skel', Buffer.from('\0' + '3.8.1\0')); item('ATLAS', 'hero.atlas', 'textures/hero.png\nsize: 64,64\n'); const texture = item('TEXTURE', 'hero.png', 'texture'); const assets = utils.discoverAssets(skeleton, { strictStem: true, refreshIndex: true }); assert.equal(assets.texturePaths['textures/hero.png'], texture, 'cross-item texture must come from a validated Eagle item'); const maliciousDir = path.join(root, 'malicious'); fs.mkdirSync(maliciousDir); const maliciousSkeleton = path.join(maliciousDir, 'evil.skel'); fs.writeFileSync(maliciousSkeleton, Buffer.from('\0' + '3.8.1\0')); fs.writeFileSync(path.join(maliciousDir, 'evil.atlas'), '../../../secret.png\nsize: 64,64\n'); assert.throws(() => utils.discoverAssets(maliciousSkeleton), (error) => error && error.code === 'UNSAFE_ATLAS_PATH'); const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '../manifest.json'), 'utf8')); const preview = Object.values(manifest.preview || {}); assert.equal(preview.some((entry) => entry && entry.thumbnail), false, 'import-time thumbnail handler must stay disabled'); assert.equal(fs.existsSync(path.join(__dirname, '../thumbnail/spine.js')), false); assert.equal(fs.existsSync(path.join(__dirname, '../js/spine-reconcile-worker.js')), false); const viewerSource = fs.readFileSync(path.join(__dirname, '../viewer/spine.html'), 'utf8'); assert.match(viewerSource, /currentItem\.data\.customThumbnail === true && !window\.confirm/); assert.match(viewerSource, /activeAssets\.skeletonPath[\s\S]*activeAssets\.atlasPath[\s\S]*Object\.values\(activeAssets\.texturePaths/); assert.match(viewerSource, /activeAssets\.skeletonCandidatePaths[\s\S]*activeAssets\.atlasCandidatePaths[\s\S]*utils\.normalizeStem/); assert.match(viewerSource, /selected: item\.data\.customThumbnail !== true && !utils\.isTextureFile\(sourcePath\)/); const groupThumbnailSource = fs.readFileSync(path.join(__dirname, '../js/spine-group-thumbnails.js'), 'utf8'); assert.match(groupThumbnailSource, /fresh\.data\.customThumbnail === true && target\.allowOverwrite !== true\) \{ skipped\+\+; continue; \}[\s\S]*options\.setThumbnail\(fresh\.id/); assert.match(groupThumbnailSource, /itemTempPath = path\.join\(os\.tmpdir\(\), 'spineviewer-group-' \+ fresh\.id[\s\S]*options\.setThumbnail\(fresh\.id, itemTempPath\)/); assert.match(viewerSource, /groupThumbBtn[\s\S]*thumbBtn/); assert.match(viewerSource, /path\.join\(os\.tmpdir\(\), 'spineviewer-current-'/); assert.doesNotMatch(viewerSource, /fs\.writeFileSync\(outPath/); console.log('PASS: atlas path boundaries, validated Eagle pairing, no import thumbnail handler, and manual current-item confirmation'); } finally { fs.rmSync(root, { recursive: true, force: true }); }