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

+228
View File
@@ -0,0 +1,228 @@
// Package cfl reads and writes CFL files, IMVU's product container: a product's files in one archive, as the
// client downloads and caches them.
//
// Layout (little-endian): "CFL3", u32 directory offset, u32 directory size (uncompressed). Each entry's data is a
// u32 stored length and the stored bytes. At the directory offset: u32 compression flag, u32 stored length, the
// directory. Each directory entry: i32 size, i32 offset (of the entry's length field), i32 compression flag, i16
// name length, the name (Latin-1). "DFL3" files are the same, with each directory entry followed by an i32 length
// and that many bytes of content hash.
package cfl
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"unicode/utf8"
)
// Compression flags.
const (
None = 0
LZMA = 4 // pylzma's format: 5 property bytes, then the raw stream
)
// Entry is one file in a CFL.
type Entry struct {
Name string
Data []byte
}
// ErrLZMA is returned for CFLs with LZMA-compressed entries or directories.
var ErrLZMA = errors.New("cfl: LZMA-compressed CFLs aren't supported")
const headerSize = 12
// Write writes entries as a CFL, in order and uncompressed. The output depends only on the entries.
func Write(w io.Writer, entries []Entry) error {
var body, dir bytes.Buffer
offset := int64(headerSize)
for _, e := range entries {
name, err := Latin1(e.Name)
if err != nil {
return err
}
if len(name) == 0 {
return errors.New("cfl: empty entry name")
}
if len(name) > math.MaxInt16 {
return fmt.Errorf("cfl: entry name %q is too long", e.Name)
}
if offset+4+int64(len(e.Data)) > math.MaxInt32 {
return errors.New("cfl: entries over 2 GiB")
}
le32(&dir, uint32(len(e.Data)))
le32(&dir, uint32(offset))
le32(&dir, None)
binary.Write(&dir, binary.LittleEndian, int16(len(name)))
dir.Write(name)
le32(&body, uint32(len(e.Data)))
body.Write(e.Data)
offset += 4 + int64(len(e.Data))
}
if offset+8+int64(dir.Len()) > math.MaxInt32 {
return errors.New("cfl: entries over 2 GiB")
}
var head bytes.Buffer
head.WriteString("CFL3")
le32(&head, uint32(offset))
le32(&head, uint32(dir.Len()))
le32(&body, None)
le32(&body, uint32(dir.Len()))
for _, b := range [][]byte{head.Bytes(), body.Bytes(), dir.Bytes()} {
if _, err := w.Write(b); err != nil {
return err
}
}
return nil
}
func le32(b *bytes.Buffer, v uint32) {
b.Write(binary.LittleEndian.AppendUint32(nil, v))
}
// Read reads a CFL (or DFL) whose entries and directory are uncompressed. Entries' Data share memory with data.
func Read(data []byte) ([]Entry, error) {
if len(data) < headerSize {
return nil, errors.New("cfl: too short")
}
var hashes bool
switch string(data[:4]) {
case "CFL3":
case "DFL3":
hashes = true
default:
return nil, fmt.Errorf("cfl: unknown header %q", data[:4])
}
dirOffset := int64(binary.LittleEndian.Uint32(data[4:]))
dirSize := int64(binary.LittleEndian.Uint32(data[8:]))
r := reader{data: data, pos: dirOffset}
flag := r.u32()
stored := int64(r.u32())
if r.err != nil {
return nil, fmt.Errorf("cfl: directory offset %d is past the end", dirOffset)
}
switch flag {
case None:
case LZMA:
return nil, ErrLZMA
default:
return nil, fmt.Errorf("cfl: directory has unknown compression %d", flag)
}
if stored != dirSize {
return nil, fmt.Errorf("cfl: directory is %d bytes, header says %d", stored, dirSize)
}
dir := r.bytes(stored)
if r.err != nil {
return nil, errors.New("cfl: directory runs past the end")
}
var entries []Entry
d := reader{data: dir}
for d.pos < int64(len(dir)) {
size := int64(int32(d.u32()))
offset := int64(int32(d.u32()))
flag := d.u32()
nameLen := int64(int16(d.u16()))
if nameLen < 0 {
return nil, fmt.Errorf("cfl: directory entry %d has a negative name length", len(entries))
}
name := d.bytes(nameLen)
if hashes {
hashLen := int64(int32(d.u32()))
if hashLen < 0 {
return nil, fmt.Errorf("cfl: directory entry %d has a negative hash length", len(entries))
}
d.bytes(hashLen)
}
if d.err != nil {
return nil, fmt.Errorf("cfl: directory entry %d is cut off", len(entries))
}
if len(name) == 0 {
return nil, fmt.Errorf("cfl: directory entry %d has no name", len(entries))
}
n := latin1String(name)
switch flag {
case None:
case LZMA:
return nil, ErrLZMA
default:
return nil, fmt.Errorf("cfl: %q has unknown compression %d", n, flag)
}
if size < 0 || offset < 0 {
return nil, fmt.Errorf("cfl: %q has a negative size or offset", n)
}
e := reader{data: data, pos: offset}
stored := int64(e.u32())
body := e.bytes(stored)
if e.err != nil {
return nil, fmt.Errorf("cfl: %q runs past the end", n)
}
if stored != size {
return nil, fmt.Errorf("cfl: %q is %d bytes, directory says %d", n, stored, size)
}
entries = append(entries, Entry{Name: n, Data: body})
}
return entries, nil
}
// reader reads little-endian values from data, setting err instead of reading past the end.
type reader struct {
data []byte
pos int64
err error
}
func (r *reader) bytes(n int64) []byte {
if r.err != nil || n < 0 || r.pos < 0 || r.pos > int64(len(r.data)) || n > int64(len(r.data))-r.pos {
r.err = io.ErrUnexpectedEOF
return nil
}
b := r.data[r.pos : r.pos+n : r.pos+n]
r.pos += n
return b
}
func (r *reader) u32() uint32 {
b := r.bytes(4)
if b == nil {
return 0
}
return binary.LittleEndian.Uint32(b)
}
func (r *reader) u16() uint16 {
b := r.bytes(2)
if b == nil {
return 0
}
return binary.LittleEndian.Uint16(b)
}
// Latin1 encodes s as Latin-1, as CFLs store names.
func Latin1(s string) ([]byte, error) {
if !utf8.ValidString(s) {
return nil, fmt.Errorf("cfl: name %q isn't valid UTF-8", s)
}
b := make([]byte, 0, len(s))
for _, r := range s {
if r > 0xff {
return nil, fmt.Errorf("cfl: name %q has characters outside Latin-1", s)
}
b = append(b, byte(r))
}
return b, nil
}
func latin1String(b []byte) string {
r := make([]rune, len(b))
for i, c := range b {
r[i] = rune(c)
}
return string(r)
}