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]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 02:52:59 -07:00
commit 9bdc3ff455
16 files changed
+1638

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
package pack
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
// Index is what T5DE reads from a product's index.xml, IMVU's product template. Other elements are ignored.
type Index struct {
Rating string // ProductRating: "GA" (general) or "AP" (adult)
Name string // ProductName
Gender string // ProductGender
TypeID int64 // ProductTypeID, 0 if absent
BodyPattern int64 // BodyPatternProductId, 0 if absent
Parent int64 // the product it derives from (__DATAIMPORT product://<id>/index.xml), 0 if none
}
// ParseIndex parses index.xml.
func ParseIndex(data []byte) (*Index, error) {
d := xml.NewDecoder(bytes.NewReader(data))
d.Strict = false
d.Entity = xml.HTMLEntity
d.CharsetReader = charsetReader
values := map[string]string{}
depth := 0
root := false
var field string // the top-level element being read, if wanted
var text strings.Builder
for {
tok, err := d.Token()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
switch t := tok.(type) {
case xml.StartElement:
depth++
if depth == 1 && t.Name.Local != "Template" {
return nil, fmt.Errorf("root element is <%s>, not <Template>", t.Name.Local)
}
root = true
if depth == 2 && wanted[t.Name.Local] {
if _, ok := values[t.Name.Local]; ok {
return nil, fmt.Errorf("<%s> appears twice", t.Name.Local)
}
field = t.Name.Local
text.Reset()
}
case xml.EndElement:
if depth == 2 && field != "" {
values[field] = strings.TrimSpace(text.String())
field = ""
}
depth--
case xml.CharData:
if field != "" {
text.Write(t)
}
}
}
if !root {
return nil, errors.New("no <Template>")
}
var x Index
x.Rating = values["ProductRating"]
x.Name = values["ProductName"]
x.Gender = values["ProductGender"]
if x.Rating != "GA" && x.Rating != "AP" {
return nil, fmt.Errorf("ProductRating: %q must be GA or AP", x.Rating)
}
var err error
if x.TypeID, err = id(values, "ProductTypeID"); err != nil {
return nil, err
}
if x.BodyPattern, err = id(values, "BodyPatternProductId"); err != nil {
return nil, err
}
if imp := values["__DATAIMPORT"]; imp != "" {
s, ok := strings.CutPrefix(imp, "product://")
if ok {
s, ok = strings.CutSuffix(s, "/index.xml")
}
n, err := strconv.ParseInt(s, 10, 64)
if !ok || err != nil || n < 1 {
return nil, fmt.Errorf("__DATAIMPORT: %q isn't product://<id>/index.xml", imp)
}
x.Parent = n
}
return &x, nil
}
var wanted = map[string]bool{
"ProductRating": true,
"ProductName": true,
"ProductGender": true,
"ProductTypeID": true,
"BodyPatternProductId": true,
"__DATAIMPORT": true,
}
func id(values map[string]string, name string) (int64, error) {
s := values[name]
if s == "" {
return 0, nil
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || n < 0 {
return 0, fmt.Errorf("%s: %q isn't a number", name, s)
}
return n, nil
}
// charsetReader reads Latin-1 (and windows-1252, approximately) as well as UTF-8.
func charsetReader(charset string, r io.Reader) (io.Reader, error) {
switch strings.ToLower(charset) {
case "iso-8859-1", "latin1", "latin-1", "windows-1252", "cp1252", "us-ascii", "ascii":
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
var b strings.Builder
for _, c := range data {
b.WriteRune(rune(c))
}
return strings.NewReader(b.String()), nil
}
return nil, fmt.Errorf("unsupported encoding %q", charset)
}