mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
1109 lines
37 KiB
Go
1109 lines
37 KiB
Go
package car
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/blacktop/go-macho/types"
|
|
"github.com/blacktop/go-termimg"
|
|
"github.com/blacktop/ipsw/internal/utils"
|
|
"github.com/blacktop/ipsw/pkg/bom"
|
|
)
|
|
|
|
// NOTES:
|
|
// - https://github.com/insidegui/AssetCatalogTinkerer
|
|
// - https://github.com/bartoszj/acextract
|
|
// - https://blog.timac.org/2018/1018-reverse-engineering-the-car-file-format/
|
|
|
|
type Config struct {
|
|
Output string
|
|
Export bool
|
|
Verbose bool
|
|
}
|
|
|
|
type Asset struct {
|
|
Header
|
|
Metadata extendedMetadata
|
|
KeyFormat []renditionAttributeType
|
|
ImageDB []Rendition
|
|
ColorDB map[string]color.RGBA
|
|
// FontDB map[string]Font
|
|
// FontSizeDB map[string]uint32
|
|
// GlyphDB map[string]Glyph
|
|
// BezelDB map[string]Bezel
|
|
FacetKeyDB map[string]renditionKeyToken
|
|
BitmapKeyDB map[any][]byte
|
|
AppearanceDB map[string]uint16
|
|
Globals []byte // bplist data
|
|
Localizations map[string]uint32
|
|
|
|
conf *Config
|
|
}
|
|
|
|
func (a *Asset) GetName(id uint16) string {
|
|
for k, v := range a.FacetKeyDB {
|
|
for _, attr := range v.Attributes {
|
|
if attr.Name == uint16(Identifier) && attr.Value == id {
|
|
return k
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (a *Asset) GetFaceKey(id uint16) (*renditionKeyToken, error) {
|
|
for _, v := range a.FacetKeyDB {
|
|
for _, attr := range v.Attributes {
|
|
if attr.Name == 17 && attr.Value == id {
|
|
return &v, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("could not find face key for id: %d", id)
|
|
}
|
|
|
|
type Header struct {
|
|
Tag [4]byte // 'CTAR'
|
|
CoreUiVersion uint32
|
|
StorageVersion uint32
|
|
StorageTimestamp uint32
|
|
RenditionCount uint32
|
|
MainVersionString [128]byte
|
|
VersionString [256]byte
|
|
UUID types.UUID
|
|
AssociatedChecksum uint32
|
|
SchemaVersion uint32
|
|
ColorSpaceID colorSpaceID
|
|
KeySemantics uint32
|
|
}
|
|
|
|
type extendedMetadata struct {
|
|
Tag [4]byte // 'META'
|
|
ThinningArguments [256]byte
|
|
DeploymentPlatformVersion [256]byte
|
|
DeploymentPlatform [256]byte
|
|
AuthoringTool [256]byte
|
|
}
|
|
|
|
type renditionAttributeType uint32
|
|
|
|
const (
|
|
ThemeLook renditionAttributeType = iota
|
|
Element
|
|
Part
|
|
Size
|
|
Direction
|
|
Placeholder
|
|
Value
|
|
ThemeAppearance
|
|
Dimension1
|
|
Dimension2
|
|
State
|
|
Layer
|
|
Scale
|
|
Localization
|
|
PresentationState
|
|
Idiom
|
|
Subtype
|
|
Identifier
|
|
PreviousValue
|
|
PreviousState
|
|
HorizontalSizeClass
|
|
VerticalSizeClass
|
|
MemoryLevelClass
|
|
GraphicsFeatureSetClass
|
|
DisplayGamut
|
|
DeploymentTarget
|
|
GlyphWeight
|
|
GlyphSize
|
|
)
|
|
|
|
type RenditionKeyformat struct {
|
|
Tag [4]byte // 'kfmt'
|
|
Version uint32
|
|
MaximumRenditionKeyTokenCount uint32
|
|
RenditionKeyTokens []renditionAttributeType
|
|
}
|
|
|
|
type systemColor struct {
|
|
Version uint32 // 1
|
|
Unknown uint32 // 0
|
|
Color struct {
|
|
Blue uint8
|
|
Green uint8
|
|
Red uint8
|
|
Alpha uint8
|
|
}
|
|
}
|
|
|
|
type renditionLayoutType uint16
|
|
|
|
const (
|
|
OnePart renditionLayoutType = 0
|
|
ThreePartHorizontal renditionLayoutType = 1
|
|
ThreePartVertical renditionLayoutType = 2
|
|
NinePart renditionLayoutType = 3
|
|
TwelvePart renditionLayoutType = 4
|
|
ManyPart renditionLayoutType = 5
|
|
Gradient renditionLayoutType = 6
|
|
Effect renditionLayoutType = 7
|
|
Animation renditionLayoutType = 8
|
|
Vector renditionLayoutType = 9
|
|
|
|
IconImage renditionLayoutType = 12
|
|
|
|
Unknown renditionLayoutType = 20
|
|
|
|
RawData renditionLayoutType = 1000
|
|
ExternalLink renditionLayoutType = 1001
|
|
ImageStack renditionLayoutType = 1002
|
|
InternalLink renditionLayoutType = 1003
|
|
PackedImage renditionLayoutType = 1004
|
|
NamedContents renditionLayoutType = 1005
|
|
ThinningPlaceholder renditionLayoutType = 1006
|
|
TextureRendition renditionLayoutType = 1007
|
|
TextureImage renditionLayoutType = 1008
|
|
Color renditionLayoutType = 1009
|
|
MultiSizeImageSet renditionLayoutType = 1010
|
|
ModelIOAsset renditionLayoutType = 1011
|
|
ModelMesh renditionLayoutType = 1012
|
|
RecognitionGroup renditionLayoutType = 1013
|
|
RecognitionObject renditionLayoutType = 1014
|
|
|
|
ModelIOSubmesh renditionLayoutType = 1016
|
|
VectorGlyph renditionLayoutType = 1017
|
|
SolidImageStack renditionLayoutType = 1018
|
|
IconImageStack renditionLayoutType = 1019
|
|
IconGroup renditionLayoutType = 1020
|
|
NamedGradient renditionLayoutType = 1021
|
|
)
|
|
|
|
type coreThemeIdiom uint32
|
|
|
|
const (
|
|
Universal coreThemeIdiom = 0
|
|
Phone coreThemeIdiom = 1
|
|
Tablet coreThemeIdiom = 2
|
|
Desktop coreThemeIdiom = 3
|
|
Tv coreThemeIdiom = 4
|
|
Car coreThemeIdiom = 5
|
|
Watch coreThemeIdiom = 6
|
|
Marketing coreThemeIdiom = 7
|
|
)
|
|
|
|
type renditionAttribute struct {
|
|
Name uint16
|
|
Value uint16
|
|
}
|
|
type renditionKeyToken struct {
|
|
CursorHotSpot struct {
|
|
X uint16
|
|
Y uint16
|
|
}
|
|
NumberOfAttributes uint16
|
|
Attributes []renditionAttribute
|
|
}
|
|
|
|
func (k *renditionKeyToken) UnmarshalBinary(data []byte) error {
|
|
r := bytes.NewReader(data)
|
|
if err := binary.Read(r, binary.LittleEndian, &k.CursorHotSpot); err != nil {
|
|
return fmt.Errorf("failed to read rendition key token cursor hotspot: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &k.NumberOfAttributes); err != nil {
|
|
return fmt.Errorf("failed to read rendition key token number of attributes: %v", err)
|
|
}
|
|
k.Attributes = make([]renditionAttribute, k.NumberOfAttributes)
|
|
for i := range k.Attributes {
|
|
if err := binary.Read(r, binary.LittleEndian, &k.Attributes[i]); err != nil {
|
|
return fmt.Errorf("failed to read rendition key token attribute %d: %v", i, err)
|
|
}
|
|
}
|
|
if r.Len() > 0 {
|
|
return fmt.Errorf("failed to read entire rendition key token: %v", r.Len())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Rendition struct {
|
|
RenditionName string
|
|
Type string
|
|
Colorspace string
|
|
Size int
|
|
Attributes map[string]uint16
|
|
Resources []csiResource
|
|
Asset any
|
|
}
|
|
|
|
func (r Rendition) ID() uint16 {
|
|
if id, ok := r.Attributes[Identifier.String()]; ok {
|
|
return id
|
|
}
|
|
return 0
|
|
}
|
|
func (r Rendition) Part() uint16 {
|
|
if id, ok := r.Attributes[Part.String()]; ok {
|
|
return id
|
|
}
|
|
return 0
|
|
}
|
|
func (r Rendition) Element() uint16 {
|
|
if id, ok := r.Attributes[Element.String()]; ok {
|
|
return id
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func Parse(name string, conf *Config) (*Asset, error) {
|
|
f, err := os.Open(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file %s: %v", name, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err := os.Stat(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to stat file %s: %v", name, err)
|
|
}
|
|
modeTime := fi.ModTime().Local().Unix()
|
|
|
|
bm, err := bom.New(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse BOM file: %v", err)
|
|
}
|
|
|
|
a := Asset{conf: conf}
|
|
|
|
a.AppearanceDB = make(map[string]uint16)
|
|
a.BitmapKeyDB = make(map[any][]byte)
|
|
a.ColorDB = make(map[string]color.RGBA)
|
|
a.Localizations = make(map[string]uint32)
|
|
|
|
if a.conf.Verbose {
|
|
log.WithField("name", name).Debug("Parsing BOM")
|
|
utils.Indent(log.Debug, 2)("Blocks/Trees: " + strings.Join(bm.BlockNames(), ", "))
|
|
}
|
|
|
|
saved := make(map[string]int)
|
|
// helpers for unique naming and saving outputs
|
|
uniqName := func(base, ext string) string {
|
|
name := base
|
|
if _, ok := saved[name]; ok {
|
|
saved[name] += 1
|
|
name += fmt.Sprintf("_%d", saved[name])
|
|
} else {
|
|
saved[name] = 0
|
|
}
|
|
if len(ext) > 0 && !strings.HasSuffix(strings.ToLower(name), strings.ToLower(ext)) {
|
|
name += ext
|
|
}
|
|
return name
|
|
}
|
|
saveBytes := func(base, ext string, data []byte) error {
|
|
if a.conf == nil || !a.conf.Export || len(a.conf.Output) == 0 {
|
|
return nil
|
|
}
|
|
fname := uniqName(base, ext)
|
|
return os.WriteFile(filepath.Join(a.conf.Output, fname), data, 0o644)
|
|
}
|
|
savePNG := func(base string, img image.Image) error {
|
|
if a.conf == nil || !a.conf.Export || len(a.conf.Output) == 0 {
|
|
return nil
|
|
}
|
|
fname := uniqName(base, ".png")
|
|
f, err := os.Create(filepath.Join(a.conf.Output, fname))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
return png.Encode(f, img)
|
|
}
|
|
|
|
for _, v := range bm.Vars {
|
|
switch v.Name {
|
|
/**********
|
|
* BLOCKS *
|
|
**********/
|
|
case "CARHEADER":
|
|
br, err := bm.ReadBlock(v.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read block %s: %v", v.Name, err)
|
|
}
|
|
if err := binary.Read(br, binary.LittleEndian, &a.Header); err != nil {
|
|
return nil, fmt.Errorf("failed to read CAR header: %v", err)
|
|
}
|
|
if a.Tag != [4]byte{'R', 'A', 'T', 'C'} { // 'CTAR'
|
|
return nil, fmt.Errorf("invalid CAR header tag: %s", a.Tag)
|
|
}
|
|
if a.StorageTimestamp == 0 {
|
|
a.StorageTimestamp = uint32(modeTime) // use file modification time
|
|
}
|
|
// TODO: read tree ? (I see a 'tree' following this data, but that might be some other object's sub-tree)
|
|
case "EXTENDED_METADATA":
|
|
br, err := bm.ReadBlock(v.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read block %s: %v", v.Name, err)
|
|
}
|
|
if err := binary.Read(br, binary.BigEndian, &a.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to read extended metadata: %v", err)
|
|
}
|
|
if a.Metadata.Tag != [4]byte{'M', 'E', 'T', 'A'} {
|
|
return nil, fmt.Errorf("invalid extended metadata tag: %s", a.Metadata.Tag)
|
|
}
|
|
case "KEYFORMAT":
|
|
if err := a.parseKeyFormat(bm); err != nil {
|
|
return nil, fmt.Errorf("failed to parse KEYFORMAT block: %v", err)
|
|
}
|
|
case "CARGLOBALS":
|
|
br, err := bm.ReadBlock(v.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read block %s: %v", v.Name, err)
|
|
}
|
|
a.Globals, err = io.ReadAll(br)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read CARGLOBALS data: %v", err)
|
|
}
|
|
case "KEYFORMATWORKAROUND":
|
|
if err := a.parseKeyFormatWorkaround(bm); err != nil {
|
|
return nil, fmt.Errorf("failed to parse KEYFORMATWORKAROUND block: %v", err)
|
|
}
|
|
if a.conf.Verbose {
|
|
br, err := bm.ReadBlock(v.Name)
|
|
if err != nil {
|
|
log.Debugf("Failed to read KEYFORMATWORKAROUND block: %v", err)
|
|
} else {
|
|
data, err := io.ReadAll(br)
|
|
if err != nil {
|
|
log.Debugf("Failed to read KEYFORMATWORKAROUND data: %v", err)
|
|
} else {
|
|
log.Debugf("KEYFORMATWORKAROUND block found (%d bytes)", len(data))
|
|
if len(data) <= 256 {
|
|
log.Debugf("KEYFORMATWORKAROUND hex dump:\n%s", hex.Dump(data))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "EXTERNAL_KEYS":
|
|
log.Error("BOM block EXTERNAL_KEYS parsing not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
/*********
|
|
* TREES *
|
|
*********/
|
|
case "APPEARANCEKEYS":
|
|
tree, err := bm.ReadTree("APPEARANCEKEYS")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read APPEARANCEKEYS tree: %v", err)
|
|
}
|
|
for _, item := range tree.Indices {
|
|
key, err := io.ReadAll(item.KeyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read key for APPEARANCEKEYS: %v", err)
|
|
}
|
|
var value uint16
|
|
if err := binary.Read(item.ValueReader, binary.LittleEndian, &value); err != nil {
|
|
return nil, fmt.Errorf("failed to read value for key %s: %v", string(key), err)
|
|
}
|
|
a.AppearanceDB[string(key)] = value
|
|
}
|
|
case "BEZELS":
|
|
log.Error("BOM tree BEZELS not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
case "BITMAPKEYS":
|
|
// NOTE: /System/Library/PrivateFrameworks/ChatKit.framework/Assets.car is SUPER weird (keys are many different types)
|
|
tree, err := bm.ReadTree("BITMAPKEYS")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read BITMAPKEYS tree: %v", err)
|
|
}
|
|
for _, item := range tree.Indices {
|
|
if a.conf.Verbose {
|
|
if err := dumpTreeIndice("BITMAPKEYS", item); err != nil {
|
|
return nil, fmt.Errorf("failed to dump BITMAPKEYS tree indice: %v", err)
|
|
}
|
|
} else {
|
|
value, err := io.ReadAll(item.ValueReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read BITMAPKEYS value: %v", err)
|
|
}
|
|
// TODO: I think if I understand the struct of the value data it might tell me what the key TYPE is
|
|
keyData, err := io.ReadAll(item.KeyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read BITMAPKEYS key data: %v", err)
|
|
}
|
|
switch {
|
|
case len(keyData) == 0:
|
|
a.BitmapKeyDB[nil] = value
|
|
case len(keyData) == 4:
|
|
var key uint32
|
|
if err := binary.Read(bytes.NewReader(keyData), binary.LittleEndian, &key); err != nil {
|
|
return nil, fmt.Errorf("failed to read BITMAPKEYS key: %v", err)
|
|
}
|
|
a.BitmapKeyDB[key] = value
|
|
case len(keyData) > 4:
|
|
// a.BitmapKeyDB[keyData] = value FIXME: this is an array of bytes (what does it represent?)
|
|
}
|
|
}
|
|
}
|
|
case "COLORS":
|
|
ctrees, err := bm.ReadTrees("COLORS")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read COLORS tree: %v", err)
|
|
}
|
|
for _, tree := range ctrees {
|
|
for _, item := range tree.Indices {
|
|
var key uint32 // always 0
|
|
if err := binary.Read(item.KeyReader, binary.LittleEndian, &key); err != nil {
|
|
return nil, fmt.Errorf("failed to read COLORS key: %v", err)
|
|
}
|
|
name, err := readString(item.KeyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read COLORS name: %v", err)
|
|
}
|
|
var sc systemColor
|
|
if err := binary.Read(item.ValueReader, binary.LittleEndian, &sc); err != nil {
|
|
return nil, fmt.Errorf("failed to read COLORS system color: %v", err)
|
|
}
|
|
a.ColorDB[name] = color.RGBA{
|
|
R: sc.Color.Red,
|
|
G: sc.Color.Green,
|
|
B: sc.Color.Blue,
|
|
A: sc.Color.Alpha,
|
|
}
|
|
}
|
|
}
|
|
case "ELEMENT_INFO":
|
|
log.Error("BOM tree ELEMENT_INFO parsing not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
case "FACETKEYS":
|
|
if err := a.parseFacetKeys(bm); err != nil {
|
|
return nil, fmt.Errorf("failed to parse FACETKEYS trees: %v", err)
|
|
}
|
|
case "FONTS":
|
|
log.Error("BOM tree FONTS parsing not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
case "FONTSIZES":
|
|
log.Error("BOM tree FONTSIZES parsing not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
case "GLYPHS":
|
|
log.Error("BOM tree GLYPHS parsing not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
case "LOCALIZATIONKEYS":
|
|
tree, err := bm.ReadTree("LOCALIZATIONKEYS")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read LOCALIZATIONKEYS tree: %v", err)
|
|
}
|
|
for _, item := range tree.Indices {
|
|
key, err := io.ReadAll(item.KeyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read LOCALIZATIONKEYS key: %v", err)
|
|
}
|
|
valueData, err := io.ReadAll(item.ValueReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read LOCALIZATIONKEYS value data: %v", err)
|
|
}
|
|
var value uint32
|
|
switch len(valueData) {
|
|
case 2:
|
|
value = uint32(binary.LittleEndian.Uint16(valueData))
|
|
case 4:
|
|
value = binary.LittleEndian.Uint32(valueData)
|
|
default:
|
|
return nil, fmt.Errorf("failed to read LOCALIZATIONKEYS value: %v; data=\n%s", err, hex.Dump(valueData))
|
|
}
|
|
a.Localizations[string(key)] = uint32(value)
|
|
}
|
|
case "PART_INFO":
|
|
log.Error("BOM tree PART_INFO parsing not implemented yet - please open an issue on github.com/blacktop/ipsw/issues")
|
|
case "RENDITIONS":
|
|
if err := a.parseKeyFormat(bm); err != nil {
|
|
return nil, fmt.Errorf("failed to parse asset KeyFormat %v", err)
|
|
}
|
|
rtrees, err := bm.ReadTrees("RENDITIONS")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' trees: %v", err)
|
|
}
|
|
for _, tree := range rtrees {
|
|
for _, item := range tree.Indices {
|
|
rend := Rendition{
|
|
Attributes: make(map[string]uint16),
|
|
}
|
|
// parse key data
|
|
keyData, err := io.ReadAll(item.KeyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' key data: %v", err)
|
|
}
|
|
attrs := make([]uint16, len(keyData)/binary.Size(uint16(0)))
|
|
if err := binary.Read(bytes.NewReader(keyData), binary.LittleEndian, &attrs); err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' key attributes: %v", err)
|
|
}
|
|
for idx, k := range a.KeyFormat {
|
|
rend.Attributes[k.String()] = attrs[idx]
|
|
}
|
|
// TODO: this might be wasteful if we don't need to read the whole thing
|
|
// parse value data
|
|
vdata, err := io.ReadAll(item.ValueReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' value data: %v", err)
|
|
}
|
|
vr := bytes.NewReader(vdata)
|
|
cheader, err := readCSIFileHeader(vr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' csiHeader: %v", err)
|
|
}
|
|
resourceData := make([]byte, cheader.ChainSize)
|
|
if _, err := vr.Read(resourceData); err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' resource data: %v", err)
|
|
}
|
|
rr := bytes.NewReader(resourceData)
|
|
for rr.Len() > 0 {
|
|
var rsc csiResource
|
|
if err := binary.Read(rr, binary.LittleEndian, &rsc.ID); err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' resource ID: %v", err)
|
|
}
|
|
if err := binary.Read(rr, binary.LittleEndian, &rsc.Length); err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' resource length: %v", err)
|
|
}
|
|
rsc.Data = make([]byte, rsc.Length)
|
|
if _, err := rr.Read(rsc.Data); err != nil {
|
|
return nil, fmt.Errorf("failed to read 'RENDITIONS' resource data: %v", err)
|
|
}
|
|
rend.Resources = append(rend.Resources, rsc)
|
|
}
|
|
|
|
if rendName, _, ok := bytes.Cut(cheader.Metadata.Name[:], []byte{0}); ok {
|
|
rend.RenditionName = string(rendName)
|
|
} else {
|
|
rend.RenditionName = string(bytes.TrimRight(cheader.Metadata.Name[:], "\x00"))
|
|
}
|
|
if len(rend.RenditionName) == 0 {
|
|
rend.RenditionName = "CoreStructuredImage"
|
|
}
|
|
rend.Type = cheader.Metadata.Layout.String()
|
|
rend.Size = int(cheader.ImageIndex.AccumLength[len(cheader.ImageIndex.AccumLength)-1])
|
|
|
|
if vr.Len() > 0 {
|
|
pixelFormat := string(utils.ReverseBytes(cheader.PixelFormat[:]))
|
|
// log.WithFields(log.Fields{
|
|
// "rendition": rend.RenditionName,
|
|
// "pixelFormat": pixelFormat,
|
|
// }).Info("RENDITION pixel format")
|
|
switch pixelFormat {
|
|
case PixFmtARGB, PixFmtARGB16, PixFmtRGB555, PixFmtGray, PixFmtGray16:
|
|
rend.Type = fmt.Sprintf("Image (%s)", cheader.Metadata.Layout.String())
|
|
if a.conf.Verbose {
|
|
log.WithFields(log.Fields{
|
|
"rendition": rend.RenditionName,
|
|
"layout": cheader.Metadata.Layout,
|
|
}).Debug("IMAGE layout")
|
|
}
|
|
switch cheader.Metadata.Layout {
|
|
case PackedImage:
|
|
// // PackedImage has a special structure - the image data is embedded differently
|
|
// // For now, log that it's a packed image and skip decoding
|
|
// log.Debugf("PackedImage layout for %s - special handling needed", rend.RenditionName)
|
|
// // Store the raw data as the asset
|
|
// data, err := io.ReadAll(vr)
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("failed to read PackedImage data: %v", err)
|
|
// }
|
|
// er := bytes.NewReader(data)
|
|
// var elem csiElement
|
|
// if err := binary.Read(er, binary.LittleEndian, &elem); err != nil {
|
|
// return nil, fmt.Errorf("failed to read PackedImage element: %v", err)
|
|
// }
|
|
// if elem.Signature != [4]byte{'M', 'L', 'E', 'C'} { // 'MLEC'
|
|
// return nil, fmt.Errorf("invalid PackedImage signature: %s", elem.Signature)
|
|
// }
|
|
// edata := make([]byte, elem.Length)
|
|
// if _, err := io.ReadFull(er, edata); err != nil {
|
|
// return nil, fmt.Errorf("failed to read LZFSE PackedImage data: %v", err)
|
|
// }
|
|
// switch elem.Encoding {
|
|
// case RLE:
|
|
// rend.Asset = decodeRLE(edata)
|
|
// case LZFSE:
|
|
// decompressedData, err := comp.Decompress(edata, comp.LZFSE)
|
|
// if err != nil {
|
|
// return nil, fmt.Errorf("failed to decompress LZFSE PackedImage data: %v", err)
|
|
// }
|
|
// rend.Asset = decompressedData
|
|
// default:
|
|
// log.WithField("encoding", elem.Encoding).Warnf("Unsupported PackedImage encoding for %s", rend.RenditionName)
|
|
// rend.Asset = edata
|
|
// }
|
|
// TODO: handle other layout special cases
|
|
log.WithField("layout", cheader.Metadata.Layout).Debugf("Unsupported PackedImage layout for %s", rend.RenditionName)
|
|
fallthrough
|
|
default:
|
|
// Extract optional ImageRowBytes from resources for correct stride
|
|
var rowBytes uint32
|
|
for _, rsc := range rend.Resources {
|
|
if rsc.ID == ImageRowBytesID && len(rsc.Data) >= 4 {
|
|
_ = binary.Read(bytes.NewReader(rsc.Data), binary.LittleEndian, &rowBytes)
|
|
break
|
|
}
|
|
}
|
|
img, err := decodeImage(vr, *cheader, conf, int(rowBytes))
|
|
if err != nil {
|
|
if a.conf.Verbose {
|
|
log.Errorf("failed to decode image '%s': %v; data:\n%s", rend.RenditionName, err, hex.Dump(vdata))
|
|
} else {
|
|
log.Errorf("failed to decode image '%s': %v", rend.RenditionName, err)
|
|
}
|
|
// return nil, err
|
|
}
|
|
if img != nil && err == nil {
|
|
if err := savePNG(rend.RenditionName, img); err != nil {
|
|
return nil, err
|
|
}
|
|
if a.conf.Verbose {
|
|
// display image in terminal
|
|
log.Debug(rend.RenditionName)
|
|
var dat bytes.Buffer
|
|
buf := bufio.NewWriter(&dat)
|
|
if err := png.Encode(buf, img); err != nil {
|
|
return nil, err
|
|
}
|
|
buf.Flush()
|
|
ti, err := termimg.From(bytes.NewReader(dat.Bytes()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create termimg from image: %v", err)
|
|
}
|
|
if err := ti.Print(); err != nil {
|
|
return nil, fmt.Errorf("failed to print termimg: %v", err)
|
|
}
|
|
}
|
|
rend.Asset = img
|
|
}
|
|
}
|
|
case PixFmtPDF:
|
|
rend.Type = "PDF"
|
|
// Read PDF data
|
|
pdfData, err := io.ReadAll(vr)
|
|
if err != nil {
|
|
log.Errorf("failed to read PDF data: %v", err)
|
|
} else {
|
|
// Store raw PDF data as asset
|
|
rend.Asset = pdfData
|
|
if err := saveBytes(rend.RenditionName, ".pdf", pdfData); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case PixFmtJPEG:
|
|
rend.Type = "JPEG"
|
|
// Read JPEG data
|
|
jpegData, err := io.ReadAll(vr)
|
|
if err != nil {
|
|
log.Errorf("failed to read JPEG data: %v", err)
|
|
} else {
|
|
// Store raw JPEG data as asset
|
|
rend.Asset = jpegData
|
|
if err := saveBytes(rend.RenditionName, ".jpg", jpegData); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case PixFmtHEIF:
|
|
rend.Type = "HEIF"
|
|
// Read HEIF data
|
|
heifData, err := io.ReadAll(vr)
|
|
if err != nil {
|
|
log.Errorf("failed to read HEIF data: %v", err)
|
|
} else {
|
|
// Store raw HEIF data as asset
|
|
rend.Asset = heifData
|
|
if err := saveBytes(rend.RenditionName, ".heic", heifData); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
case PixFmtRawData:
|
|
rend.Type = "Data"
|
|
var rawd csiRawData
|
|
if err := binary.Read(vr, binary.LittleEndian, &rawd.Signature); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition raw data signature: %v", err)
|
|
}
|
|
if rawd.Signature != [4]byte{'D', 'W', 'A', 'R'} { // 'RAWD'
|
|
return nil, fmt.Errorf("invalid rendition raw data signature: %s", rawd.Signature)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &rawd.Flags); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition raw data flags: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &rawd.Length); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition raw data length: %v", err)
|
|
}
|
|
if rawd.Length != uint32(vr.Len()) {
|
|
return nil, fmt.Errorf("rendition raw data length mismatch: expected %d, got %d", rawd.Length, vr.Len())
|
|
}
|
|
if rawd.Length > 0 {
|
|
if a.conf != nil && a.conf.Export && len(a.conf.Output) > 0 {
|
|
data := make([]byte, rawd.Length)
|
|
if _, err := vr.Read(data); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition raw data: %v", err)
|
|
}
|
|
fname := a.GetName(rend.ID())
|
|
if len(fname) == 0 {
|
|
fname = rend.RenditionName + ".raw"
|
|
}
|
|
if err := os.WriteFile(filepath.Join(a.conf.Output, fname), data, 0644); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
case "\x00\x00\x00\x00":
|
|
// log.WithFields(log.Fields{
|
|
// "name": rend.RenditionName,
|
|
// "name2": string(bytes.Trim(cheader.Metadata.Name[:], "\x00")),
|
|
// "layout": cheader.Metadata.Layout,
|
|
// }).Info("RENDITION layout")
|
|
switch cheader.Metadata.Layout {
|
|
case OnePart, ThreePartHorizontal, ThreePartVertical, NinePart, TwelvePart, ManyPart:
|
|
fallthrough
|
|
case Gradient:
|
|
fallthrough
|
|
case Effect:
|
|
fallthrough
|
|
case Animation:
|
|
fallthrough
|
|
case Vector:
|
|
fallthrough
|
|
case IconImage:
|
|
fallthrough
|
|
case RawData:
|
|
fallthrough
|
|
case ExternalLink:
|
|
fallthrough
|
|
case ImageStack:
|
|
log.Errorf("RENDITION layout %s not supported yet - please open an issue on github.com/blacktop/ipsw/issues", cheader.Metadata.Layout)
|
|
case InternalLink:
|
|
var ilink csiInternalLinkData
|
|
if err := ilink.UnmarshalBinary(vr); err != nil {
|
|
return nil, fmt.Errorf("failed to parse rendition internal link: %v", err)
|
|
}
|
|
rend.Asset = ilink
|
|
case PackedImage:
|
|
fallthrough
|
|
case NamedContents:
|
|
fallthrough
|
|
case ThinningPlaceholder:
|
|
fallthrough
|
|
case TextureRendition:
|
|
fallthrough
|
|
case TextureImage:
|
|
log.Errorf("RENDITION layout %s not supported yet - please open an issue on github.com/blacktop/ipsw/issues", cheader.Metadata.Layout)
|
|
case Color:
|
|
var c csiColor
|
|
if err := binary.Read(vr, binary.LittleEndian, &c.Signature); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition color signature: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &c.Version); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition color version: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &c.Info); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition color info: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &c.NumberOfComponents); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition color number of components: %v", err)
|
|
}
|
|
c.Components = make([]float64, c.NumberOfComponents)
|
|
if err := binary.Read(vr, binary.LittleEndian, &c.Components); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition color components: %v", err)
|
|
}
|
|
if c.Info.ColorType() == SystemColorFollows {
|
|
var sysc csiSystemColorName
|
|
if err := binary.Read(vr, binary.LittleEndian, &sysc.Signature); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition system color signature: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &sysc.Version); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition system color version: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &sysc.Length); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition system color name length: %v", err)
|
|
}
|
|
sysc.Name = make([]byte, sysc.Length)
|
|
if _, err := vr.Read(sysc.Name); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition system color name: %v", err)
|
|
}
|
|
rend.Asset = sysc // FIXME: this will stomp the outer color?
|
|
}
|
|
rend.Asset = c
|
|
case MultiSizeImageSet:
|
|
var msi csiMultisizeImageSet
|
|
if err := binary.Read(vr, binary.LittleEndian, &msi.Signature); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &msi.Version); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &msi.NImageSizes); err != nil {
|
|
return nil, err
|
|
}
|
|
msi.ImageSizes = make([]csiMultiImgSetImageSize, msi.NImageSizes)
|
|
if err := binary.Read(vr, binary.LittleEndian, &msi.ImageSizes); err != nil {
|
|
return nil, err
|
|
}
|
|
rend.Asset = msi
|
|
case ModelIOAsset:
|
|
fallthrough
|
|
case ModelMesh:
|
|
fallthrough
|
|
case RecognitionGroup:
|
|
fallthrough
|
|
case RecognitionObject:
|
|
fallthrough
|
|
case ModelIOSubmesh:
|
|
fallthrough
|
|
case VectorGlyph:
|
|
fallthrough
|
|
case SolidImageStack:
|
|
fallthrough
|
|
case IconImageStack:
|
|
fallthrough
|
|
case IconGroup:
|
|
log.Errorf("RENDITION layout %s not supported yet - please open an issue on github.com/blacktop/ipsw/issues", cheader.Metadata.Layout)
|
|
case NamedGradient:
|
|
var ng csiNamedGradient
|
|
if err := binary.Read(vr, binary.LittleEndian, &ng.Signature); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient signature: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &ng.ColorCount); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient color count: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &ng.Type); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient type: %v", err)
|
|
}
|
|
ng.StartStops = make([]gradientStartStops, ng.ColorCount)
|
|
if err := binary.Read(vr, binary.LittleEndian, &ng.StartStops); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient starts: %v", err)
|
|
}
|
|
ng.Stops = make([]gradientStop, ng.ColorCount)
|
|
for i := range ng.Stops {
|
|
if err := binary.Read(vr, binary.LittleEndian, &ng.Stops[i].Stop); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient stop color: %v", err)
|
|
}
|
|
if err := binary.Read(vr, binary.LittleEndian, &ng.Stops[i].NameLength); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient stop name length: %v", err)
|
|
}
|
|
ng.Stops[i].Name = make([]byte, ng.Stops[i].NameLength)
|
|
if _, err := vr.Read(ng.Stops[i].Name); err != nil {
|
|
return nil, fmt.Errorf("failed to read rendition named gradient stop name: %v", err)
|
|
}
|
|
}
|
|
rend.Asset = ng
|
|
default:
|
|
return nil, fmt.Errorf("unknown RENDITION layout: %d - please open an issue on github.com/blacktop/ipsw/issues", cheader.Metadata.Layout)
|
|
}
|
|
default:
|
|
log.Errorf("unknown pixel format: %s", pixelFormat)
|
|
}
|
|
}
|
|
a.ImageDB = append(a.ImageDB, rend)
|
|
}
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unknown BOM block/tree: '%s' - please open an issue on github.com/blacktop/ipsw/issues", name)
|
|
}
|
|
}
|
|
|
|
return &a, nil
|
|
}
|
|
|
|
func (a *Asset) parseFacetKeys(bm *bom.BOM) error {
|
|
if a.FacetKeyDB != nil {
|
|
return nil
|
|
}
|
|
|
|
a.FacetKeyDB = make(map[string]renditionKeyToken)
|
|
|
|
ftree, err := bm.ReadTrees("FACETKEYS")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read 'FACETKEYS' tree: %v", err)
|
|
}
|
|
|
|
for _, tree := range ftree {
|
|
for _, item := range tree.Indices {
|
|
var token renditionKeyToken
|
|
if err := binary.Read(item.ValueReader, binary.LittleEndian, &token.CursorHotSpot); err != nil {
|
|
return fmt.Errorf("failed to read 'FACETKEYS' cursor hotspot: %v", err)
|
|
}
|
|
if err := binary.Read(item.ValueReader, binary.LittleEndian, &token.NumberOfAttributes); err != nil {
|
|
return fmt.Errorf("failed to read 'FACETKEYS' number of attributes: %v", err)
|
|
}
|
|
token.Attributes = make([]renditionAttribute, token.NumberOfAttributes)
|
|
if err := binary.Read(item.ValueReader, binary.LittleEndian, &token.Attributes); err != nil {
|
|
return fmt.Errorf("failed to read 'FACETKEYS' attributes: %v", err)
|
|
}
|
|
name, err := io.ReadAll(item.KeyReader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read 'FACETKEYS' name: %v", err)
|
|
}
|
|
a.FacetKeyDB[string(name)] = token
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *Asset) parseKeyFormat(bm *bom.BOM) error {
|
|
if a.KeyFormat != nil {
|
|
return nil
|
|
}
|
|
br, err := bm.ReadBlock("KEYFORMAT")
|
|
if err != nil {
|
|
if workaroundErr := a.parseKeyFormatWorkaround(bm); workaroundErr != nil {
|
|
return fmt.Errorf("failed to read block 'KEYFORMAT': %v; fallback 'KEYFORMATWORKAROUND' parse failed: %v", err, workaroundErr)
|
|
}
|
|
return nil
|
|
}
|
|
var keyfmt RenditionKeyformat
|
|
if err := binary.Read(br, binary.LittleEndian, &keyfmt.Tag); err != nil {
|
|
return fmt.Errorf("failed to read 'KEYFORMAT' tag: %v", err)
|
|
}
|
|
if keyfmt.Tag != [4]byte{'t', 'm', 'f', 'k'} { // 'kfmt'
|
|
return fmt.Errorf("invalid 'KEYFORMAT' tag: %s", keyfmt.Tag)
|
|
}
|
|
if err := binary.Read(br, binary.LittleEndian, &keyfmt.Version); err != nil {
|
|
return fmt.Errorf("failed to read 'KEYFORMAT' version: %v", err)
|
|
}
|
|
if err := binary.Read(br, binary.LittleEndian, &keyfmt.MaximumRenditionKeyTokenCount); err != nil {
|
|
return fmt.Errorf("failed to read 'KEYFORMAT' maximum rendition key token count: %v", err)
|
|
}
|
|
a.KeyFormat = make([]renditionAttributeType, keyfmt.MaximumRenditionKeyTokenCount)
|
|
if err := binary.Read(br, binary.LittleEndian, &a.KeyFormat); err != nil {
|
|
return fmt.Errorf("failed to read 'KEYFORMAT' rendition attribute types: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Asset) parseKeyFormatWorkaround(bm *bom.BOM) error {
|
|
if a.KeyFormat != nil {
|
|
return nil
|
|
}
|
|
br, err := bm.ReadBlock("KEYFORMATWORKAROUND")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read block 'KEYFORMATWORKAROUND': %v", err)
|
|
}
|
|
|
|
var maxTokenCount uint32
|
|
if err := binary.Read(br, binary.LittleEndian, &maxTokenCount); err != nil {
|
|
return fmt.Errorf("failed to read KEYFORMATWORKAROUND max token count: %v", err)
|
|
}
|
|
if maxTokenCount == 0 {
|
|
return nil
|
|
}
|
|
|
|
a.KeyFormat = make([]renditionAttributeType, maxTokenCount)
|
|
if err := binary.Read(br, binary.LittleEndian, &a.KeyFormat); err != nil {
|
|
return fmt.Errorf("failed to read KEYFORMATWORKAROUND rendition attribute types: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TODO: this is gross
|
|
func readCSIFileHeader(r io.Reader) (*csiHeader, error) {
|
|
var c csiHeader
|
|
if err := binary.Read(r, binary.BigEndian, &c.Signature); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader signature: %v", err)
|
|
}
|
|
if c.Signature != CsiFileSignature {
|
|
return nil, fmt.Errorf("invalid csiHeader signature: %v", c.Signature)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.Version); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader version: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.Flags); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader flags: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.Width); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader width: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.Height); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader height: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.ScaleFactor); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader ppi: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.PixelFormat); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader pixel format: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.ColorSpace); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader color space: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.Metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader metadata: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.ChainSize); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader chain size: %v", err)
|
|
}
|
|
if err := binary.Read(r, binary.LittleEndian, &c.ImageIndex.Count); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader image index count: %v", err)
|
|
}
|
|
c.ImageIndex.AccumLength = make([]uint32, c.ImageIndex.Count+1)
|
|
if err := binary.Read(r, binary.LittleEndian, &c.ImageIndex.AccumLength); err != nil {
|
|
return nil, fmt.Errorf("failed to read csiHeader image index accum lengths: %v", err)
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// decodeRLE implements a PackBits-like RLE commonly used in CoreUI payloads.
|
|
// Control byte N:
|
|
// - 0..127 : copy the next (N+1) literal bytes
|
|
// - 129..255 : repeat the next byte (257 - N) times
|
|
// - 128 : no-op
|
|
func decodeRLE(src []byte) []byte {
|
|
var dst []byte
|
|
for i := 0; i < len(src); {
|
|
n := int(int8(src[i]))
|
|
i++
|
|
switch {
|
|
case n >= 0:
|
|
count := n + 1
|
|
if i+count > len(src) {
|
|
count = len(src) - i
|
|
}
|
|
dst = append(dst, src[i:i+count]...)
|
|
i += count
|
|
case n == -128:
|
|
// nop
|
|
default:
|
|
if i >= len(src) {
|
|
break
|
|
}
|
|
b := src[i]
|
|
i++
|
|
count := 1 - n
|
|
for range count {
|
|
dst = append(dst, b)
|
|
}
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func readString(r io.Reader) (string, error) {
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
return strings.Trim(scanner.Text(), "\x00"), nil
|
|
}
|
|
return "", scanner.Err()
|
|
}
|
|
|
|
func dumpTreeIndice(block string, item bom.TreeIndex) error {
|
|
keyData, err := io.ReadAll(item.KeyReader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s key: %v", block, err)
|
|
}
|
|
println(block + " KEY")
|
|
println(hex.Dump(keyData))
|
|
|
|
valueData, err := io.ReadAll(item.ValueReader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read %s value: %v", block, err)
|
|
}
|
|
println(block + " VALUE")
|
|
println(hex.Dump(valueData))
|
|
|
|
return nil
|
|
}
|