2023-08-17 23:17:01 +03:00
|
|
|
package caff
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"compress/flate"
|
|
|
|
"io"
|
|
|
|
|
|
|
|
"github.com/wader/fq/format"
|
|
|
|
"github.com/wader/fq/pkg/bitio"
|
|
|
|
"github.com/wader/fq/pkg/decode"
|
|
|
|
"github.com/wader/fq/pkg/interp"
|
|
|
|
"github.com/wader/fq/pkg/scalar"
|
|
|
|
)
|
|
|
|
|
|
|
|
var probeGroup decode.Group
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
interp.RegisterFormat(
|
|
|
|
format.CAFF,
|
|
|
|
&decode.Format{
|
|
|
|
Description: "Live2D Cubism archive",
|
|
|
|
Groups: []*decode.Group{format.Probe},
|
|
|
|
DecodeFn: decodeCAFF,
|
|
|
|
DefaultInArg: format.CAFF_In{
|
|
|
|
Uncompress: true,
|
|
|
|
},
|
|
|
|
Dependencies: []decode.Dependency{
|
|
|
|
{Groups: []*decode.Group{format.Probe}, Out: &probeGroup},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
imageFormatUnknown = 0x00
|
|
|
|
imageFormatPNG = 0x01
|
2023-08-18 17:40:54 +03:00
|
|
|
imageFormatNoPreview = 0x7f
|
2023-08-17 23:17:01 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
colorTypeUnknown = 0x00
|
|
|
|
colorTypeARGB = 0x01
|
|
|
|
colorTypeRGB = 0x02
|
2023-08-18 17:40:54 +03:00
|
|
|
colorTypeNoPreview = 0x7f
|
2023-08-17 23:17:01 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
compressOptionRaw = 0x10
|
|
|
|
compressOptionFast = 0x21
|
|
|
|
compressOptionSmall = 0x25
|
|
|
|
)
|
|
|
|
|
2023-08-19 17:07:10 +03:00
|
|
|
var bool8ToSym = scalar.UintMapSymBool{
|
|
|
|
0: false,
|
|
|
|
1: true,
|
|
|
|
}
|
|
|
|
|
2023-08-17 23:17:01 +03:00
|
|
|
var imageFormatNames = scalar.UintMapSymStr{
|
|
|
|
imageFormatUnknown: "unknown",
|
|
|
|
imageFormatPNG: "png",
|
|
|
|
imageFormatNoPreview: "no_preview",
|
|
|
|
}
|
|
|
|
|
|
|
|
var colorTypeNames = scalar.UintMapSymStr{
|
|
|
|
colorTypeUnknown: "unknown",
|
|
|
|
colorTypeARGB: "argb",
|
|
|
|
colorTypeRGB: "rgb",
|
|
|
|
colorTypeNoPreview: "no_preview",
|
|
|
|
}
|
|
|
|
|
|
|
|
var compressOptionNames = scalar.UintMapSymStr{
|
|
|
|
compressOptionRaw: "raw",
|
|
|
|
compressOptionFast: "fast",
|
|
|
|
compressOptionSmall: "small",
|
|
|
|
}
|
|
|
|
|
|
|
|
type fileInfoListEntry struct {
|
|
|
|
filePath string
|
|
|
|
startPos int64
|
|
|
|
fileSize int
|
|
|
|
isObfuscated bool
|
|
|
|
compressOption uint8
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeVersion(d *decode.D) {
|
|
|
|
d.FieldU8("major")
|
|
|
|
d.FieldU8("minor")
|
|
|
|
d.FieldU8("patch")
|
|
|
|
}
|
|
|
|
|
|
|
|
func decodeCAFF(d *decode.D) any {
|
|
|
|
var ci format.CAFF_In
|
|
|
|
d.ArgAs(&ci)
|
|
|
|
|
2023-08-19 05:45:09 +03:00
|
|
|
var obfsKey int64
|
2023-08-17 23:17:01 +03:00
|
|
|
|
2023-08-19 06:02:26 +03:00
|
|
|
obfsU8 := func(d *decode.D) uint64 { return d.U8() ^ uint64(obfsKey&0xff) }
|
|
|
|
obfsU32 := func(d *decode.D) uint64 { return d.U32() ^ uint64(obfsKey&0xffff_ffff) }
|
|
|
|
obfsU64 := func(d *decode.D) uint64 { return d.U64() ^ uint64(obfsKey<<32|obfsKey) }
|
2023-08-17 23:17:01 +03:00
|
|
|
|
|
|
|
// "Big Endian Base 128" - LEB128's strange sibling
|
|
|
|
obfsBEB128 := func(d *decode.D) (v uint64) {
|
|
|
|
for {
|
|
|
|
x := obfsU8(d)
|
|
|
|
v <<= 7
|
2023-08-18 17:40:54 +03:00
|
|
|
v |= (x & 0x7f)
|
2023-08-17 23:17:01 +03:00
|
|
|
if (x >> 7) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
obfsVarStr := func(d *decode.D) string {
|
|
|
|
length := obfsBEB128(d)
|
|
|
|
if length == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
raw := d.BytesLen(int(length))
|
|
|
|
for i := uint64(0); i < length; i++ {
|
|
|
|
raw[i] ^= byte(obfsKey)
|
|
|
|
}
|
|
|
|
return string(raw)
|
|
|
|
}
|
|
|
|
|
|
|
|
d.FieldUTF8("archive_id", 4, d.StrAssert("CAFF"))
|
|
|
|
d.FieldStruct("archive_version", decodeVersion)
|
|
|
|
d.FieldUTF8("format_id", 4)
|
|
|
|
d.FieldStruct("format_version", decodeVersion)
|
2023-08-19 05:45:09 +03:00
|
|
|
obfsKey = d.FieldS32("obfuscate_key")
|
2023-08-19 17:07:10 +03:00
|
|
|
d.FieldU64("unused0")
|
2023-08-17 23:17:01 +03:00
|
|
|
|
|
|
|
d.FieldStruct("preview_image", func(d *decode.D) {
|
|
|
|
d.FieldU8("image_format", imageFormatNames)
|
|
|
|
d.FieldU8("color_type", colorTypeNames)
|
2023-08-19 17:07:10 +03:00
|
|
|
d.FieldU16("unused0")
|
2023-08-17 23:17:01 +03:00
|
|
|
d.FieldU16("width")
|
|
|
|
d.FieldU16("height")
|
2023-08-19 17:07:10 +03:00
|
|
|
d.FieldU64("start_pos", scalar.UintHex)
|
2023-08-17 23:17:01 +03:00
|
|
|
d.FieldU32("file_size")
|
|
|
|
})
|
2023-08-19 17:07:10 +03:00
|
|
|
d.FieldU64("unused1")
|
2023-08-17 23:17:01 +03:00
|
|
|
|
|
|
|
fileInfoListSize := d.FieldUintFn("file_info_map_size", obfsU32)
|
|
|
|
fileInfoList := make([]fileInfoListEntry, int(fileInfoListSize))
|
|
|
|
|
|
|
|
d.FieldArray("file_info_list", func(d *decode.D) {
|
|
|
|
for i := uint64(0); i < fileInfoListSize; i++ {
|
|
|
|
d.FieldStruct("file_info", func(d *decode.D) {
|
|
|
|
var entry fileInfoListEntry
|
|
|
|
|
|
|
|
entry.filePath = d.FieldStrFn("file_path", obfsVarStr)
|
|
|
|
d.FieldStrFn("tag", obfsVarStr)
|
2023-08-19 17:07:10 +03:00
|
|
|
entry.startPos = int64(d.FieldUintFn("start_pos", obfsU64, scalar.UintHex))
|
2023-08-17 23:17:01 +03:00
|
|
|
entry.fileSize = int(d.FieldUintFn("file_size", obfsU32))
|
2023-08-19 17:07:10 +03:00
|
|
|
entry.isObfuscated = d.FieldUintFn("is_obfuscated", obfsU8, bool8ToSym) != 0
|
2023-08-17 23:17:01 +03:00
|
|
|
entry.compressOption = uint8(d.FieldUintFn("compress_option", obfsU8, compressOptionNames))
|
2023-08-19 17:07:10 +03:00
|
|
|
d.FieldU64("unused0")
|
2023-08-17 23:17:01 +03:00
|
|
|
|
|
|
|
fileInfoList[int(i)] = entry
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
d.FieldArray("files", func(d *decode.D) {
|
|
|
|
for _, entry := range fileInfoList {
|
|
|
|
d.FieldStruct("file", func(d *decode.D) {
|
|
|
|
d.SeekAbs(entry.startPos * 8)
|
|
|
|
d.FieldValueStr("file_path", entry.filePath)
|
|
|
|
d.FieldValueUint("file_size", uint64(entry.fileSize))
|
|
|
|
d.FieldValueBool("is_obfuscated", entry.isObfuscated)
|
|
|
|
d.FieldValueUint("compress_option", uint64(entry.compressOption), compressOptionNames)
|
|
|
|
|
2023-08-19 17:07:10 +03:00
|
|
|
rawBr := d.FieldRawLen("raw", int64(entry.fileSize) * 8)
|
|
|
|
rawBytes := make([]byte, entry.fileSize)
|
|
|
|
if n, err := rawBr.ReadBits(rawBytes, int64(entry.fileSize) * 8); err != nil || n != int64(entry.fileSize) * 8 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-17 23:17:01 +03:00
|
|
|
if entry.isObfuscated {
|
|
|
|
for i, v := range rawBytes {
|
|
|
|
rawBytes[i] = v ^ uint8(obfsKey)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
br := bitio.NewBitReader(rawBytes, -1)
|
|
|
|
if !ci.Uncompress || entry.compressOption == compressOptionRaw {
|
|
|
|
fieldName := "uncompressed"
|
|
|
|
if entry.compressOption != compressOptionRaw {
|
|
|
|
fieldName = "compressed"
|
|
|
|
}
|
|
|
|
|
|
|
|
value, _, err := d.TryFieldFormatBitBuf(fieldName, br, &probeGroup, format.Probe_In{})
|
|
|
|
if value == nil && err != nil {
|
|
|
|
d.FieldRootBitBuf(fieldName, br)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
d.FieldRootBitBuf("compressed", br)
|
|
|
|
|
|
|
|
// Offset 0x26: skip ZIP entry header; there's nothing useful in it and it's always the same
|
2023-08-18 17:40:54 +03:00
|
|
|
if len(rawBytes) > 0x26 {
|
|
|
|
infBytes, err := io.ReadAll(flate.NewReader(bytes.NewReader(rawBytes[0x26:])))
|
|
|
|
if err == nil {
|
|
|
|
infBr := bitio.NewBitReader(infBytes, -1)
|
2023-08-18 19:25:10 +03:00
|
|
|
value, _, err := d.TryFieldFormatBitBuf("uncompressed", infBr, &probeGroup, format.Probe_In{})
|
|
|
|
if value == nil && err != nil {
|
|
|
|
d.FieldRootBitBuf("uncompressed", infBr)
|
|
|
|
}
|
2023-08-18 17:40:54 +03:00
|
|
|
}
|
2023-08-17 23:17:01 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
d.SeekAbs(d.Len() - 2*8)
|
|
|
|
d.FieldRawLen("guard_bytes", 2*8, d.AssertBitBuf([]byte{98, 99}))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|