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": "
A coat
", "rating": "general"}`
testIndex = `GA` +
`10680` +
`female1,2` +
`<__DATAIMPORT>product://12345/index.xmlTest Coat` +
`nested, ignored`
)
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("")))
}},
{"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("" +
" AP Caf\xe9 & Barx
"))
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("\ufeffGA")); err != nil || x.Rating != "GA" {
t.Errorf("with a BOM: %+v, %v", x, err)
}
for _, c := range []struct{ xml, want string }{
{`GA`, "Template"},
{``, "Template"},
{``, "ProductRating"},
{`PG`, "ProductRating"},
{`GAx`, "ProductTypeID"},
{`GA-1`, "BodyPatternProductId"},
{`GA<__DATAIMPORT>http://e.com/index.xml`, "__DATAIMPORT"},
{`GA<__DATAIMPORT>product://0/index.xml`, "__DATAIMPORT"},
{`GAAP`, "twice"},
{``, "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)
}
}
}