// 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) }