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:
commit
9bdc3ff455
16 files changed
+1638
No files matched your search
+137
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user