Files
t5de-api/pack/pack_test.go
T
dhkatzandClaude Opus 5.5 9bdc3ff455 Add pack, cfl and safehtml
pack loads local product packs (format 1): t5de-product.json and the product's files, as product.chkn or
files/, with index.xml's rating, type, gender and parent product. cfl writes and reads IMVU's product
container, uncompressed. safehtml cleans product descriptions by an allowlist (bluemonday).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:52:59 -07:00

316 lines
12 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pack
import (
"archive/zip"
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"git.t5de.dev/t5de/t5de-api/cfl"
)
// Synthetic test files: not IMVU's.
const (
testManifest = `{"format": 1, "id": "blue-coat", "revision": 2, "name": " Blue Coat ", "creator": "Tester",
"description": "<p>A coat</p>", "rating": "general"}`
testIndex = `<?xml version="1.0" encoding="UTF-8" ?><Template><ProductRating>GA</ProductRating>` +
`<ProductTypeID>106</ProductTypeID><BodyPatternProductId>80</BodyPatternProductId>` +
`<ProductGender>female</ProductGender><ParentCategoryIDs>1,2</ParentCategoryIDs>` +
`<__DATAIMPORT>product://12345/index.xml</__DATAIMPORT><ProductName>Test Coat</ProductName>` +
`<Other><ProductName>nested, ignored</ProductName></Other></Template>`
)
var testPNG = []byte("\x89PNG\r\n\x1a\nfake")
func testFiles() map[string][]byte {
return map[string][]byte{"index.xml": []byte(testIndex), "coat.png": testPNG, "coat.xmf": []byte("mesh")}
}
func writeFile(t *testing.T, path string, data []byte) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatal(err)
}
}
// dirPack makes a pack with its files in files/.
func dirPack(t *testing.T, manifest string, files map[string][]byte) string {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, ManifestName), []byte(manifest))
if err := os.Mkdir(filepath.Join(dir, FilesDir), 0o755); err != nil {
t.Fatal(err)
}
for name, data := range files {
writeFile(t, filepath.Join(dir, FilesDir, name), data)
}
return dir
}
type zipEntry struct {
name string
data []byte
method uint16
}
func makeZip(t *testing.T, entries []zipEntry) []byte {
t.Helper()
var b bytes.Buffer
z := zip.NewWriter(&b)
for _, e := range entries {
w, err := z.CreateHeader(&zip.FileHeader{Name: e.name, Method: e.method})
if err != nil {
t.Fatal(err)
}
w.Write(e.data)
}
if err := z.Close(); err != nil {
t.Fatal(err)
}
return b.Bytes()
}
// chknPack makes a pack with its files in product.chkn.
func chknPack(t *testing.T, manifest string, entries []zipEntry) string {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, ManifestName), []byte(manifest))
writeFile(t, filepath.Join(dir, ChknName), makeZip(t, entries))
return dir
}
func zipEntries(method uint16) []zipEntry {
var entries []zipEntry
for name, data := range testFiles() {
entries = append(entries, zipEntry{name, data, method})
}
return entries
}
func TestLoad(t *testing.T) {
fromDir, err := Load(dirPack(t, testManifest, testFiles()))
if err != nil {
t.Fatal(err)
}
m := fromDir.Manifest
if m.ID != "blue-coat" || m.Revision != 2 || m.Name != "Blue Coat" || m.Creator != "Tester" || m.Rating != General {
t.Errorf("manifest: %+v", m)
}
want := Index{Rating: "GA", Name: "Test Coat", Gender: "female", TypeID: 106, BodyPattern: 80, Parent: 12345}
if fromDir.Index != want {
t.Errorf("index: got %+v, want %+v", fromDir.Index, want)
}
var names []string
for _, f := range fromDir.Files {
names = append(names, f.Name)
}
if got := strings.Join(names, " "); got != "coat.png coat.xmf index.xml" {
t.Errorf("files: %s", got)
}
// The same files zipped, stored or deflated, have the same checksum. Folder entries are ignored.
for _, method := range []uint16{zip.Store, zip.Deflate} {
entries := append(zipEntries(method), zipEntry{name: "folder/", method: method})
fromZip, err := Load(chknPack(t, testManifest, entries))
if err != nil {
t.Fatal(err)
}
if fromZip.Checksum() != fromDir.Checksum() {
t.Errorf("method %d: checksum differs", method)
}
}
changed := testFiles()
changed["coat.png"] = append(testPNG, 0)
other, err := Load(dirPack(t, testManifest, changed))
if err != nil {
t.Fatal(err)
}
if other.Checksum() == fromDir.Checksum() {
t.Error("checksum didn't change")
}
data, err := fromDir.CFL()
if err != nil {
t.Fatal(err)
}
entries, err := cfl.Read(data)
if err != nil || len(entries) != 3 || entries[2].Name != "index.xml" {
t.Errorf("CFL: %q, %v", entries, err)
}
}
func TestLoadErrors(t *testing.T) {
adult := strings.Replace(testManifest, `"general"`, `"adult"`, 1)
with := func(name string, data []byte) map[string][]byte {
f := testFiles()
f[name] = data
return f
}
without := func(name string) map[string][]byte {
f := testFiles()
delete(f, name)
return f
}
extra := func(e ...zipEntry) []zipEntry { return append(zipEntries(zip.Store), e...) }
for _, c := range []struct {
name, want string
dir func(t *testing.T) string
}{
{"rating mismatch", "ProductRating", func(t *testing.T) string { return dirPack(t, adult, testFiles()) }},
{"no index", "no index.xml", func(t *testing.T) string { return dirPack(t, testManifest, without("index.xml")) }},
{"contents json", "_contents.json", func(t *testing.T) string {
return dirPack(t, testManifest, with("_contents.json", []byte("{}")))
}},
{"bad index", "index.xml: ProductRating", func(t *testing.T) string {
return dirPack(t, testManifest, with("index.xml", []byte("<Template/>")))
}},
{"subfolder", "folders", func(t *testing.T) string {
dir := dirPack(t, testManifest, testFiles())
writeFile(t, filepath.Join(dir, FilesDir, "sub", "a.png"), testPNG)
return dir
}},
{"both", "both exist", func(t *testing.T) string {
dir := dirPack(t, testManifest, testFiles())
writeFile(t, filepath.Join(dir, ChknName), makeZip(t, zipEntries(zip.Store)))
return dir
}},
{"neither", "no product.chkn", func(t *testing.T) string {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, ManifestName), []byte(testManifest))
return dir
}},
{"no manifest", ManifestName, func(t *testing.T) string { return t.TempDir() }},
{"bad manifest", "revision", func(t *testing.T) string {
return dirPack(t, strings.Replace(testManifest, `"revision": 2`, `"revision": 0`, 1), testFiles())
}},
{"nested zip entry", "in a folder", func(t *testing.T) string {
return chknPack(t, testManifest, extra(zipEntry{"sub/a.png", testPNG, zip.Store}))
}},
{"backslash in zip", `can't have`, func(t *testing.T) string {
return chknPack(t, testManifest, extra(zipEntry{`sub\a.png`, testPNG, zip.Store}))
}},
{"duplicate in zip", "duplicate", func(t *testing.T) string {
return chknPack(t, testManifest, extra(zipEntry{"coat.png", testPNG, zip.Store}))
}},
{"not latin-1", "Latin-1", func(t *testing.T) string {
return chknPack(t, testManifest, extra(zipEntry{"snow☃.png", testPNG, zip.Store}))
}},
{"too many files", "over 512 files", func(t *testing.T) string {
var e []zipEntry
for i := range MaxFiles {
e = append(e, zipEntry{name: fmt.Sprintf("f%d.png", i), method: zip.Store})
}
return chknPack(t, testManifest, extra(e...))
}},
{"zip bomb", "over 64 MiB", func(t *testing.T) string {
// Deflates to well under 1 MiB.
return chknPack(t, testManifest, extra(zipEntry{"big.png", make([]byte, MaxFileSize+1), zip.Deflate}))
}},
{"too big in all", "in all", func(t *testing.T) string {
var e []zipEntry
for i := range 5 {
e = append(e, zipEntry{fmt.Sprintf("big%d.png", i), make([]byte, MaxFileSize), zip.Deflate})
}
return chknPack(t, testManifest, extra(e...))
}},
{"not a zip", ChknName, func(t *testing.T) string {
dir := t.TempDir()
writeFile(t, filepath.Join(dir, ManifestName), []byte(testManifest))
writeFile(t, filepath.Join(dir, ChknName), []byte("not a zip"))
return dir
}},
} {
t.Run(c.name, func(t *testing.T) {
_, err := Load(c.dir(t))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("got %v, want an error with %q", err, c.want)
}
})
}
}
func TestParseManifest(t *testing.T) {
for _, c := range []struct{ json, want string }{
{`{"format": 2, "id": "a", "revision": 1, "name": "A", "rating": "general"}`, "format"},
{`{"id": "a", "revision": 1, "name": "A", "rating": "general"}`, "format"},
{`{"format": 1, "id": "-a", "revision": 1, "name": "A", "rating": "general"}`, "id"},
{`{"format": 1, "id": "a-", "revision": 1, "name": "A", "rating": "general"}`, "id"},
{`{"format": 1, "id": "A", "revision": 1, "name": "A", "rating": "general"}`, "id"},
{`{"format": 1, "id": "", "revision": 1, "name": "A", "rating": "general"}`, "id"},
{`{"format": 1, "id": "` + strings.Repeat("a", 65) + `", "revision": 1, "name": "A", "rating": "general"}`, "id"},
{`{"format": 1, "id": "a", "revision": 0, "name": "A", "rating": "general"}`, "revision"},
{`{"format": 1, "id": "a", "revision": 1.5, "name": "A", "rating": "general"}`, "revision"},
{`{"format": 1, "id": "a", "revision": 1, "name": " ", "rating": "general"}`, "name"},
{`{"format": 1, "id": "a", "revision": 1, "name": "` + strings.Repeat("é", 65) + `", "rating": "general"}`, "name"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A\u0007", "rating": "general"}`, "name"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A", "creator": "` + strings.Repeat("c", 65) + `", "rating": "general"}`, "creator"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A", "description": "` + strings.Repeat("d", 20001) + `", "rating": "general"}`, "description"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A", "rating": "GA"}`, "rating"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A"}`, "rating"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A", "rating": "general", "price": 5}`, "price"},
{`{"format": 1, "id": "a", "revision": 1, "name": "A", "rating": "general"} {}`, "after"},
} {
_, err := ParseManifest([]byte(c.json))
if err == nil || !strings.Contains(err.Error(), c.want) || !strings.Contains(err.Error(), ManifestName) {
t.Errorf("%s: got %v, want an error with %q", c.json, err, c.want)
}
}
m, err := ParseManifest([]byte(`{"format": 1, "id": "a-1", "revision": 1, "name": "` + strings.Repeat("é", 64) + `", "rating": "adult"}`))
if err != nil || m.Rating != Adult {
t.Errorf("valid manifest: %+v, %v", m, err)
}
}
func TestParseIndex(t *testing.T) {
x, err := ParseIndex([]byte("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><Template>" +
"<ProductRating> AP </ProductRating><ProductName>Caf\xe9 &amp; &nbsp;Bar</ProductName><Junk a=b>x<br></Junk></Template>"))
if err != nil {
t.Fatal(err)
}
if want := (Index{Rating: "AP", Name: "Café &  Bar"}); *x != want {
t.Errorf("got %+v, want %+v", *x, want)
}
if x, err := ParseIndex([]byte("\ufeff<Template><ProductRating>GA</ProductRating></Template>")); err != nil || x.Rating != "GA" {
t.Errorf("with a BOM: %+v, %v", x, err)
}
for _, c := range []struct{ xml, want string }{
{`<Product><ProductRating>GA</ProductRating></Product>`, "Template"},
{``, "Template"},
{`<Template></Template>`, "ProductRating"},
{`<Template><ProductRating>PG</ProductRating></Template>`, "ProductRating"},
{`<Template><ProductRating>GA</ProductRating><ProductTypeID>x</ProductTypeID></Template>`, "ProductTypeID"},
{`<Template><ProductRating>GA</ProductRating><BodyPatternProductId>-1</BodyPatternProductId></Template>`, "BodyPatternProductId"},
{`<Template><ProductRating>GA</ProductRating><__DATAIMPORT>http://e.com/index.xml</__DATAIMPORT></Template>`, "__DATAIMPORT"},
{`<Template><ProductRating>GA</ProductRating><__DATAIMPORT>product://0/index.xml</__DATAIMPORT></Template>`, "__DATAIMPORT"},
{`<Template><ProductRating>GA</ProductRating><ProductRating>AP</ProductRating></Template>`, "twice"},
{`<?xml version="1.0" encoding="EBCDIC"?><Template/>`, "encoding"},
} {
_, err := ParseIndex([]byte(c.xml))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: got %v, want an error with %q", c.xml, err, c.want)
}
}
}
func TestCheckName(t *testing.T) {
for _, name := range []string{"index.xml", "a b.png", "café.jpg", strings.Repeat("a", 200)} {
if err := CheckName(name); err != nil {
t.Errorf("%q: %v", name, err)
}
}
for _, name := range []string{"", ".", "..", "a/b", `a\b`, "\xff", strings.Repeat("a", 201), "_CONTENTS.json", "a\x00b", "a\nb", "snow☃"} {
if err := CheckName(name); err == nil {
t.Errorf("%q: no error", name)
}
}
}