mirror of
https://github.com/blacktop/ipsw.git
synced 2026-05-08 12:22:26 +00:00
8e6f2c554b
Refactor string concatenation to use strings.Builder for improved performance in multiple files
547 lines
15 KiB
Go
547 lines
15 KiB
Go
package bundle
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/asn1"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/apex/log"
|
|
"github.com/blacktop/go-macho/types"
|
|
)
|
|
|
|
const Magic = "BUND"
|
|
|
|
type Header struct {
|
|
Unknown1 uint16 // 0x0200
|
|
Unknown2 uint16 // 0x1400
|
|
_ uint32 // padding ?
|
|
Magic [4]byte // "BUND"
|
|
_ uint16 // padding ?
|
|
Type uint16 // 3 (AOP/DCP), 4 (ExclaveCore)
|
|
}
|
|
|
|
type Bundle struct {
|
|
Header
|
|
TypeHeader any
|
|
Config Config
|
|
Files []File
|
|
|
|
r io.ReadSeeker
|
|
closer any
|
|
}
|
|
|
|
func (b Bundle) String() string {
|
|
var s strings.Builder
|
|
s.WriteString(fmt.Sprintf("Bundle: %s\n", string(b.Magic[:])))
|
|
s.WriteString(fmt.Sprintf(" Type: %d\n", b.Type))
|
|
switch b.Type {
|
|
case 3:
|
|
s.WriteString(" Config:\n")
|
|
s.WriteString(fmt.Sprintf(" Unk1: %d\n", b.Config.Unk1))
|
|
s.WriteString(fmt.Sprintf(" Unk2: %d\n", b.Config.Unk2))
|
|
s.WriteString(" Assets:\n")
|
|
for i, h := range b.Config.Assets {
|
|
s.WriteString(fmt.Sprintf(" %3s) %-20s\n", fmt.Sprintf("%d", i+1), h))
|
|
}
|
|
s.WriteString(" TOC:\n")
|
|
for _, t := range b.Config.TOC {
|
|
s.WriteString(fmt.Sprintf(" %s\n", t))
|
|
}
|
|
s.WriteString("Compartments:\n")
|
|
for _, f := range b.Files {
|
|
s.WriteString(fmt.Sprintf("%s\n", f))
|
|
}
|
|
case 4:
|
|
s.WriteString(" Ranges:\n")
|
|
for i, f := range b.TypeHeader.(Type4).Ranges {
|
|
s.WriteString(fmt.Sprintf(" %3s) %s\n", fmt.Sprintf("%d", i+1), f))
|
|
}
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
type Segment struct {
|
|
Name string
|
|
Offset uint64
|
|
Size uint64
|
|
}
|
|
|
|
type Section struct {
|
|
Name string
|
|
Offset uint64
|
|
Size uint64
|
|
}
|
|
|
|
type File struct {
|
|
Name string
|
|
Type string
|
|
Segments []Segment
|
|
Sections []Section
|
|
Endpoints []Endpoint
|
|
Extra map[string]any
|
|
}
|
|
|
|
func (f File) Segment(name string) *Segment {
|
|
for _, seg := range f.Segments {
|
|
if seg.Name == name {
|
|
return &seg
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f File) String() string {
|
|
var s strings.Builder
|
|
s.WriteString(fmt.Sprintf(" %s (%s)\n", f.Name, f.Type))
|
|
for _, seg := range f.Segments {
|
|
if seg.Size == 0 {
|
|
continue
|
|
}
|
|
if seg.Name == "HEADER" {
|
|
s.WriteString(fmt.Sprintf(" sz=0x%08x off=0x%08x-0x%08x __%s\n", seg.Size, seg.Offset, seg.Offset+seg.Size, seg.Name))
|
|
}
|
|
}
|
|
for _, sec := range f.Sections {
|
|
if sec.Size == 0 {
|
|
continue
|
|
}
|
|
s.WriteString(fmt.Sprintf(" sz=0x%08x off=0x%08x-0x%08x __%s\n", sec.Size, sec.Offset, sec.Offset+sec.Size, sec.Name))
|
|
}
|
|
if len(f.Endpoints) > 0 {
|
|
s.WriteString(" endpoints:\n")
|
|
for i, ep := range f.Endpoints {
|
|
s.WriteString(fmt.Sprintf(" %3s) %s\n", fmt.Sprintf("%d", i+1), ep))
|
|
}
|
|
}
|
|
if len(f.Extra) > 0 {
|
|
s.WriteString(" extra:\n")
|
|
for k, v := range f.Extra {
|
|
s.WriteString(fmt.Sprintf(" %s: %v\n", k, v))
|
|
}
|
|
}
|
|
return s.String()
|
|
}
|
|
|
|
type Type3 struct {
|
|
UUID types.UUID
|
|
_ [4]uint64 // padding ?
|
|
SubType uint64
|
|
_ uint64 // padding ?
|
|
FileSz uint64
|
|
_ uint64 // padding ?
|
|
EndOff uint64
|
|
DataOff uint64 // roottask __DATA offset
|
|
_ uint64 // padding ?
|
|
KernelDataOffset uint64 // 2D0C000h
|
|
_ uint64 // padding ?
|
|
TextOff uint64 // roottask __TEXT offset
|
|
UnkSz uint64
|
|
TextOffAgain uint64 // ???
|
|
TextSz uint64 // is x14000 again
|
|
UnkSzMaybe uint64 // 2CF8000h
|
|
_ uint64 // padding ?
|
|
UnkOffMaybe uint64 // 5FC000h
|
|
UnkNumOfImgs uint64 // C0000000h ? maybe just 0xC == 11 ?
|
|
What uint64 // C0035404h
|
|
UnkSz2 uint64 // 5FC000h
|
|
Something uint64 // 26FC000h
|
|
KernelDataOff uint64 // 2D0C000h
|
|
SomeSize2 uint64 // 334000h
|
|
_ uint64 // padding ?
|
|
AnotherSz uint64 // 30000h ?
|
|
_ uint64 // padding ?
|
|
_ uint64 // padding ?
|
|
YouAgain uint64 // 30000h ?
|
|
UnkSize4 uint64 // 74000h ?
|
|
UnkSize5 uint64 // 40000h ?
|
|
HelpMe uint64 // C05FC000h ?
|
|
UnkSize6 uint64 // 264000h ?
|
|
Nooooooo uint64 // 14000h ???
|
|
SweetBaaaby uint64 // C0670000h ??
|
|
ImDead uint64 // 70000h ??
|
|
_ uint64 // padding ?
|
|
Aaaagain uint64 // 70000h ??
|
|
UnkOffset6 uint64 // 2BC000h ???
|
|
UnkOffset7 uint64 // 2D0C000h ???
|
|
UnkOffset8 uint64 // 334000h ???
|
|
Aaaaaaaaaagin uint64 // 70000h ???
|
|
_ uint64 // padding ?
|
|
Aaaaaaaaaagin2 uint64 // 70000h ???
|
|
UnkOffset9 uint64 // 2BC000h ???
|
|
UnkOffset10 uint64 // 32C000h ???
|
|
FooterOffset uint64
|
|
FooterSz uint64
|
|
}
|
|
|
|
type Type4 struct {
|
|
Unk0 uint32 // 1
|
|
Unk1 uint32 // 0xc == 11
|
|
_ uint64 // padding ?
|
|
Unk2 uint64 // F000h
|
|
_ [4]uint64 // padding ?
|
|
Unk3 uint64 // C000h
|
|
_ [2]uint64 // padding ?
|
|
Unk4 uint64 // 3 ?
|
|
_ uint64 // padding ?
|
|
Unk5 uint64 // 16000h ?
|
|
_ uint64 // padding ?
|
|
_ uint64 // padding ?
|
|
NumRanges uint64 // 0xD == 12 ?
|
|
UUID types.UUID // 636B62C3-4647-34F7-9089-A58256078A27
|
|
_ uint64 // padding ?
|
|
Unk7 uint64 // 14000h ?
|
|
Unk7again uint64 // 14000h ?
|
|
Unk8 uint64 // 8000000h ?
|
|
Unk9 uint64 // 1 ?
|
|
_ uint64 // padding ?
|
|
Unk10 uint64 // C000h ?
|
|
Unk10again uint64 // C000h ?
|
|
Unk11 uint64 // 8014000h ?
|
|
Unk12 uint64 // 4 ?
|
|
_ [3]uint64 // padding ?
|
|
Unk13 uint64 // 8020000h ?
|
|
Unk14 uint64 // 6 ?
|
|
Unk15 uint64 // 8003E80h ?
|
|
_ [36]uint64 // padding ?
|
|
Unk17 uint64 // 0xa == 10 ?
|
|
_ uint64 // padding ?
|
|
Unk18 uint64 // 16000h ?
|
|
Unk19 uint64 // 1582Ch ?
|
|
_ uint64 // padding ?
|
|
Unk20 uint64 // 0xa == 10 ?
|
|
_ uint64 // padding ?
|
|
Unk21 uint64 // F000h ?
|
|
Ranges [0xD]typ4Range // FIXME: this should be read AFTER the Type4 header is read
|
|
}
|
|
|
|
type typ4Range struct {
|
|
Type uint32
|
|
Name [4]byte
|
|
Offset uint64
|
|
Size uint64
|
|
}
|
|
|
|
func (t4 typ4Range) String() string {
|
|
slices.Reverse(t4.Name[:])
|
|
if t4.Size == 0 {
|
|
return fmt.Sprintf("typ=%d sz=%-10s off=0x%08x-0x%08x %s", t4.Type, fmt.Sprintf("%d", t4.Size), t4.Offset, t4.Offset+t4.Size, t4.Name)
|
|
}
|
|
return fmt.Sprintf("typ=%d sz=0x%08x off=0x%08x-0x%08x %s", t4.Type, t4.Size, t4.Offset, t4.Offset+t4.Size, t4.Name)
|
|
}
|
|
|
|
type Config struct {
|
|
Unk1 int
|
|
Unk2 int
|
|
Assets []Asset
|
|
TOC []TocEntry
|
|
Compartments []Compartment
|
|
}
|
|
|
|
type Asset struct {
|
|
Raw asn1.RawContent
|
|
Name asn1.RawValue
|
|
Type int
|
|
Offset int
|
|
Size int
|
|
}
|
|
|
|
func (h Asset) String() string {
|
|
return fmt.Sprintf("%15s type=%d off=%#07x sz=%#x", h.Name.Bytes, h.Type, h.Offset, h.Size)
|
|
}
|
|
|
|
type TocEntry struct {
|
|
Index int
|
|
Entry asn1.RawValue `asn1:"optional"`
|
|
}
|
|
|
|
type TocEntryType struct {
|
|
Name asn1.RawValue
|
|
Type int
|
|
}
|
|
|
|
func (t TocEntry) GetEntry() *TocEntryType {
|
|
if len(t.Entry.Bytes) > 0 {
|
|
var typ TocEntryType
|
|
if _, err := asn1.Unmarshal(t.Entry.Bytes, &typ); err == nil {
|
|
return &typ
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t TocEntry) String() string {
|
|
if entry := t.GetEntry(); entry != nil {
|
|
return fmt.Sprintf("%3d) %15s type=%d", t.Index, entry.Name.Bytes, entry.Type)
|
|
}
|
|
return fmt.Sprintf("%3d) %s", t.Index, "nil")
|
|
}
|
|
|
|
type Compartment struct {
|
|
Raw asn1.RawContent
|
|
AppUID int
|
|
Metadata []metadata
|
|
}
|
|
|
|
type metadata struct {
|
|
Raw asn1.RawContent
|
|
Key asn1.RawValue
|
|
Value asn1.RawValue
|
|
}
|
|
|
|
type Endpoint struct {
|
|
Type int
|
|
Unk1 asn1.RawValue
|
|
Unk2 asn1.RawValue
|
|
Name asn1.RawValue
|
|
}
|
|
|
|
func (e Endpoint) String() string {
|
|
return string(e.Name.Bytes)
|
|
}
|
|
|
|
func (md metadata) ParseValue() (any, error) {
|
|
if bytes.HasPrefix(md.Key.Bytes, []byte("__COMPONENT")) {
|
|
return string(md.Value.Bytes), nil
|
|
}
|
|
if bytes.HasPrefix(md.Key.Bytes, []byte("__ENDPOINT")) {
|
|
var e Endpoint
|
|
if _, err := asn1.Unmarshal(md.Value.Bytes, &e); err == nil {
|
|
return e, nil
|
|
} else {
|
|
return nil, fmt.Errorf("failed to unmarshal bundle file info value: %v", err)
|
|
}
|
|
}
|
|
if len(md.Value.Bytes) <= 8 {
|
|
var num uint64
|
|
for idx, b := range md.Value.Bytes {
|
|
num |= uint64(b) << (8 * uint64(len(md.Value.Bytes)-1-idx))
|
|
}
|
|
return num, nil
|
|
}
|
|
return md.Value.Bytes, nil
|
|
}
|
|
|
|
func (md metadata) String() string {
|
|
val, err := md.ParseValue()
|
|
if err != nil {
|
|
return fmt.Sprintf("[ERROR] failed to parse value: %v", err)
|
|
}
|
|
switch v := val.(type) {
|
|
case string:
|
|
return fmt.Sprintf("%s: %s", string(md.Key.Bytes), v)
|
|
case Endpoint:
|
|
return fmt.Sprintf("%s: %s", string(md.Key.Bytes), v)
|
|
case uint64:
|
|
if len(md.Value.Bytes) == 1 {
|
|
return fmt.Sprintf("%s: %d", string(md.Key.Bytes), v)
|
|
}
|
|
return fmt.Sprintf("%s: %#x", string(md.Key.Bytes), v)
|
|
default:
|
|
return fmt.Sprintf("%s: %v", string(md.Key.Bytes), v)
|
|
}
|
|
}
|
|
|
|
func (b *Bundle) ParseFiles() error {
|
|
for _, bf := range b.Config.Compartments {
|
|
var f File
|
|
var sec Section
|
|
var seg Segment
|
|
entpoints := make(map[int]Endpoint, 0)
|
|
f.Extra = make(map[string]any)
|
|
for _, md := range bf.Metadata {
|
|
val, err := md.ParseValue()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse bundle file info value: %v", err)
|
|
}
|
|
if strings.EqualFold(string(md.Key.Bytes), "__COMPONENTNAME") {
|
|
f.Name = val.(string)
|
|
} else if strings.EqualFold(string(md.Key.Bytes), "__COMPONENTTYPE") {
|
|
f.Type = val.(string)
|
|
} else if _, secpart, ok := strings.Cut(string(md.Key.Bytes), "__MACHO__"); ok { // SECTION
|
|
if name, _, ok := strings.Cut(secpart, "OFF"); ok {
|
|
sec.Name = name
|
|
sec.Offset = val.(uint64)
|
|
}
|
|
if name, _, ok := strings.Cut(secpart, "SZ"); ok {
|
|
if sec.Name == "" {
|
|
sec.Name = name
|
|
}
|
|
sec.Size = val.(uint64)
|
|
}
|
|
if sec.Name != "" && sec.Size != 0 {
|
|
f.Sections = append(f.Sections, sec)
|
|
sec = Section{}
|
|
}
|
|
} else if _, segpart, ok := strings.Cut(string(md.Key.Bytes), "__MACHO"); ok { // SEGMENT
|
|
if name, _, ok := strings.Cut(segpart, "OFF"); ok {
|
|
seg.Name = name
|
|
seg.Offset = val.(uint64)
|
|
}
|
|
if name, _, ok := strings.Cut(segpart, "SZ"); ok {
|
|
if seg.Name == "" {
|
|
seg.Name = name
|
|
}
|
|
seg.Size = val.(uint64)
|
|
}
|
|
if seg.Name != "" && seg.Size != 0 {
|
|
f.Segments = append(f.Segments, seg)
|
|
seg = Segment{}
|
|
}
|
|
} else if _, idx, ok := strings.Cut(string(md.Key.Bytes), "__ENDPOINT__"); ok { // ENDPOINT
|
|
if i, err := strconv.Atoi(idx); err == nil {
|
|
entpoints[i] = val.(Endpoint)
|
|
} else {
|
|
return fmt.Errorf("failed to parse bundle file endpoint index: %v", err)
|
|
}
|
|
} else {
|
|
f.Extra[string(md.Key.Bytes)] = string(md.Value.Bytes)
|
|
}
|
|
}
|
|
sort.Slice(f.Sections, func(i, j int) bool {
|
|
return f.Sections[i].Offset < f.Sections[j].Offset
|
|
})
|
|
sort.Slice(f.Segments, func(i, j int) bool {
|
|
return f.Segments[i].Offset < f.Segments[j].Offset
|
|
})
|
|
keys := make([]int, 0, len(entpoints))
|
|
for k := range entpoints {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Ints(keys)
|
|
for _, k := range keys {
|
|
f.Endpoints = append(f.Endpoints, entpoints[k])
|
|
}
|
|
b.Files = append(b.Files, f)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bundle) DumpFiles(output string) error {
|
|
if b.Type == 4 {
|
|
for _, rng := range b.TypeHeader.(Type4).Ranges {
|
|
slices.Reverse(rng.Name[:])
|
|
if rng.Size > 0 {
|
|
if _, err := b.r.Seek(int64(rng.Offset), io.SeekStart); err != nil {
|
|
return fmt.Errorf("failed to seek to bundle config data: %v", err)
|
|
}
|
|
data := make([]byte, rng.Size)
|
|
if _, err := b.r.Read(data); err != nil {
|
|
return fmt.Errorf("failed to read bundle data: %v", err)
|
|
}
|
|
fname := filepath.Join(output, string(rng.Name[:])+".bin")
|
|
log.WithField("name", fname).Info("Creating")
|
|
if err := os.WriteFile(fname, data, 0o644); err != nil {
|
|
return fmt.Errorf("failed to write bundle data to file: %v", err)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
return fmt.Errorf("unsupported bundle type: %d", b.Type)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Open(filename string) (*Bundle, error) {
|
|
f, err := os.Open(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open file: %v", err)
|
|
}
|
|
bundle, err := Parse(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
bundle.closer = f
|
|
return bundle, nil
|
|
}
|
|
|
|
func (b *Bundle) Close() error {
|
|
if b.closer != nil {
|
|
err := b.closer.(io.Closer).Close()
|
|
b.closer = nil
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Parse(r io.ReadSeeker) (*Bundle, error) {
|
|
var bn Bundle
|
|
bn.r = r
|
|
|
|
if err := binary.Read(r, binary.LittleEndian, &bn.Header); err != nil {
|
|
return nil, fmt.Errorf("failed to read bundle header: %v", err)
|
|
}
|
|
slices.Reverse(bn.Magic[:])
|
|
|
|
if string(bn.Magic[:]) != Magic {
|
|
return nil, fmt.Errorf("invalid magic: %s; expected 'BUND'", string(bn.Magic[:]))
|
|
}
|
|
|
|
switch bn.Type {
|
|
case 3: // ExclaveCore
|
|
var t3 Type3
|
|
if err := binary.Read(r, binary.LittleEndian, &t3); err != nil {
|
|
return nil, fmt.Errorf("failed to read bundle type 3: %v", err)
|
|
}
|
|
bn.TypeHeader = t3
|
|
// parse footer/config
|
|
if _, err := r.Seek(-int64(t3.FooterOffset), io.SeekEnd); err != nil {
|
|
return nil, fmt.Errorf("failed to seek to bundle config data: %v", err)
|
|
}
|
|
|
|
fdata := make([]byte, t3.FooterSz)
|
|
if _, err := r.Read(fdata); err != nil {
|
|
return nil, fmt.Errorf("failed to read bundle data: %v", err)
|
|
}
|
|
|
|
if _, err := asn1.Unmarshal(fdata, &bn.Config); err != nil {
|
|
return nil, fmt.Errorf("failed to ASN.1 parse bundle config: %v", err)
|
|
}
|
|
|
|
if err := bn.ParseFiles(); err != nil {
|
|
return nil, fmt.Errorf("failed to parse bundle files: %v", err)
|
|
}
|
|
case 4: // AOP/DCP
|
|
var t4 Type4
|
|
if err := binary.Read(r, binary.LittleEndian, &t4); err != nil {
|
|
return nil, fmt.Errorf("failed to read bundle type 4: %v", err)
|
|
}
|
|
bn.TypeHeader = t4
|
|
for _, rng := range t4.Ranges {
|
|
slices.Reverse(rng.Name[:])
|
|
switch string(rng.Name[:]) {
|
|
// case "uedt":
|
|
// // parse device tree/config
|
|
// log.WithField("name", string(rng.Name[:])).Debug("Device Tree")
|
|
// if _, err := r.Seek(int64(rng.Offset), io.SeekStart); err != nil {
|
|
// return nil, fmt.Errorf("failed to seek to bundle config data: %v", err)
|
|
// }
|
|
// dtdata := make([]byte, rng.Size)
|
|
// if _, err := r.Read(dtdata); err != nil {
|
|
// return nil, fmt.Errorf("failed to read bundle data: %v", err)
|
|
// }
|
|
// dt, err := devicetree.ParseData(bytes.NewReader(dtdata))
|
|
// if err != nil {
|
|
// os.WriteFile("uedt.bin", dtdata, 0o644)
|
|
// return nil, fmt.Errorf("failed to parse device tree: %v", err)
|
|
// }
|
|
// log.Debug(dt.String())
|
|
default:
|
|
log.WithField("name", string(rng.Name[:])).Debug("unsupported")
|
|
}
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unknown bundle type: %d", bn.Type)
|
|
}
|
|
|
|
return &bn, nil
|
|
}
|