Files
ipsw/internal/commands/macho/objc.go

1371 lines
40 KiB
Go

// Package macho provides functionality for parsing Mach-O files.
package macho
import (
"cmp"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"syscall"
"github.com/alecthomas/chroma/v2/quick"
"github.com/apex/log"
"github.com/blacktop/go-macho"
"github.com/blacktop/go-macho/pkg/swift"
"github.com/blacktop/go-macho/types"
"github.com/blacktop/go-macho/types/objc"
"github.com/blacktop/go-plist"
"github.com/blacktop/ipsw/pkg/dyld"
"github.com/blacktop/ipsw/pkg/tbd"
)
// ErrNoObjc is returned when a MachO does not contain objc info
var ErrNoObjc = errors.New("macho does not contain objc info")
var baseFrameworks = []string{"Foundation", "CoreFoundation", "libobjc.A.dylib"}
// ObjcConfig for MachO ObjC parser
type ObjcConfig struct {
Name string
Verbose bool
Addrs bool
Headers bool
ObjcRefs bool
Deps bool
Generic bool
Demangle bool
IpswVersion string
Color bool
Theme string
Output string
}
// Imports represents the imported symbols, local symbols, classes, and protocols for a ObjC header
type Imports struct {
Imports []string
Locals []string
Classes []string
Protos []string
}
func (i *Imports) uniq(foundation map[string][]string) {
slices.Sort(i.Imports)
slices.Sort(i.Locals)
slices.Sort(i.Classes)
slices.Sort(i.Protos)
i.Imports = slices.Compact(i.Imports)
i.Locals = slices.Compact(i.Locals)
i.Classes = slices.Compact(i.Classes)
i.Protos = slices.Compact(i.Protos)
i.Imports = slices.DeleteFunc(i.Imports, func(l string) bool {
l = strings.TrimSuffix(l, "-Protocol.h")
l = strings.TrimSuffix(l, ".h")
_, foundC := slices.BinarySearch(foundation["classes"], l)
_, foundP := slices.BinarySearch(foundation["protocols"], l)
return foundC || foundP
})
i.Locals = slices.DeleteFunc(i.Locals, func(l string) bool {
l = strings.TrimSuffix(l, "-Protocol.h")
l = strings.TrimSuffix(l, ".h")
_, foundC := slices.BinarySearch(foundation["classes"], l)
_, foundP := slices.BinarySearch(foundation["protocols"], l)
return foundC || foundP
})
// remove Foundation classes
i.Classes = slices.DeleteFunc(i.Classes, func(c string) bool {
_, found := slices.BinarySearch(foundation["classes"], c)
return found
})
// remove Foundation protocols
i.Protos = slices.DeleteFunc(i.Protos, func(p string) bool {
_, found := slices.BinarySearch(foundation["protocols"], p)
return found
})
}
type headerInfo struct {
FileName string
IpswVersion string
BuildVersions []string
SourceVersion string
IsUmbrella bool
Name string
Imports Imports
Object string
}
func (o *ObjC) maybeDemangle(text string) string {
if !o.conf.Demangle {
return text
}
return swift.DemangleBlob(text)
}
type objcDescriber interface {
Verbose() string
WithAddrs() string
}
func (o *ObjC) verboseOrAddrs(v objcDescriber) string {
if o.conf.Addrs {
return o.maybeDemangle(v.WithAddrs())
}
return o.maybeDemangle(v.Verbose())
}
// ObjC represents a MachO ObjC parser
type ObjC struct {
conf *ObjcConfig
file *macho.File
cache *dyld.File
deps []*macho.File
baseFWs map[string][]string
}
// NewObjC returns a new MachO ObjC parser instance
func NewObjC(file *macho.File, dsc *dyld.File, conf *ObjcConfig) (*ObjC, error) {
if !file.HasObjC() {
return nil, ErrNoObjc
}
o := &ObjC{
conf: conf,
file: file,
cache: dsc,
baseFWs: make(map[string][]string),
}
if o.conf.Deps {
if dsc == nil {
return nil, fmt.Errorf("dyld shared cache is required to dump imported private frameworks")
}
deps, err := loadImportedMachODependencies(
o.cache,
o.conf.Name,
file.ImportedLibraries(),
o.conf.Headers,
)
if err != nil {
return nil, err
}
o.deps = deps
}
return o, nil
}
// DumpClass returns a ObjC classes matching a given pattern from a MachO
func (o *ObjC) DumpClass(pattern string) error {
re, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("failed to compile regex: %v", err)
}
ms := []*macho.File{o.file}
if o.conf.Deps {
ms = append(ms, o.deps...)
}
for _, m := range ms {
classes, err := m.GetObjCClasses()
if err != nil {
if errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
return err
}
slices.SortStableFunc(classes, func(a, b objc.Class) int {
return cmp.Compare(a.Name, b.Name)
})
for _, class := range classes {
if re.MatchString(class.Name) {
if o.conf.Color {
quick.Highlight(os.Stdout, o.verboseOrAddrs(&class), "objc", "terminal256", o.conf.Theme)
quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(o.verboseOrAddrs(&class))
}
}
}
}
return nil
}
// DumpProtocol returns a ObjC protocols matching a given pattern from a MachO
func (o *ObjC) DumpProtocol(pattern string) error {
re, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("failed to compile regex: %v", err)
}
ms := []*macho.File{o.file}
if o.conf.Deps {
ms = append(ms, o.deps...)
}
for _, m := range ms {
protos, err := m.GetObjCProtocols()
if err != nil {
if errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
return err
}
slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
return cmp.Compare(a.Name, b.Name)
})
seen := make(map[uint64]bool)
for _, proto := range protos {
if re.MatchString(proto.Name) {
if _, ok := seen[proto.Ptr]; !ok { // prevent displaying duplicates
if o.conf.Color {
quick.Highlight(os.Stdout, o.verboseOrAddrs(&proto), "objc", "terminal256", o.conf.Theme)
quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(o.verboseOrAddrs(&proto))
}
seen[proto.Ptr] = true
}
}
}
}
return nil
}
// DumpCategory returns a ObjC categories matching a given pattern from a MachO
func (o *ObjC) DumpCategory(pattern string) error {
re, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("failed to compile regex: %v", err)
}
ms := []*macho.File{o.file}
if o.conf.Deps {
ms = append(ms, o.deps...)
}
for _, m := range ms {
cats, err := m.GetObjCCategories()
if err != nil {
if errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
return err
}
slices.SortStableFunc(cats, func(a, b objc.Category) int {
return cmp.Compare(a.Name, b.Name)
})
for _, cat := range cats {
if re.MatchString(cat.Name) {
if o.conf.Color {
quick.Highlight(os.Stdout, o.verboseOrAddrs(&cat), "objc", "terminal256", o.conf.Theme)
quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(o.verboseOrAddrs(&cat))
}
}
}
}
return nil
}
// Dump outputs ObjC info from a MachO
func (o *ObjC) Dump() error {
ms := []*macho.File{o.file}
if o.conf.Deps {
ms = append(ms, o.deps...)
}
for _, m := range ms {
if o.conf.Verbose {
if info, err := m.GetObjCImageInfo(); err == nil {
fmt.Println(info.Flags)
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
fmt.Println(m.GetObjCToc())
}
/* ObjC Protocols */
if protos, err := m.GetObjCProtocols(); err == nil {
slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
return cmp.Compare(a.Name, b.Name)
})
seen := make(map[uint64]bool)
for _, proto := range protos {
if _, ok := seen[proto.Ptr]; !ok { // prevent displaying duplicates
if o.conf.Verbose {
if o.conf.Color {
quick.Highlight(os.Stdout, o.verboseOrAddrs(&proto), "objc", "terminal256", o.conf.Theme)
quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(o.verboseOrAddrs(&proto))
}
} else {
if o.conf.Color {
quick.Highlight(os.Stdout, proto.String()+"\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(proto.String())
}
}
seen[proto.Ptr] = true
}
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
/* ObjC Classes */
if classes, err := m.GetObjCClasses(); err == nil {
slices.SortStableFunc(classes, func(a, b objc.Class) int {
return cmp.Compare(a.Name, b.Name)
})
for _, class := range classes {
if o.conf.Verbose {
if o.conf.Color {
quick.Highlight(os.Stdout, o.verboseOrAddrs(&class), "objc", "terminal256", o.conf.Theme)
quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(o.verboseOrAddrs(&class))
}
} else {
if o.conf.Color {
quick.Highlight(os.Stdout, class.String()+"\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(class.String())
}
}
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
/* ObjC Categories */
if cats, err := m.GetObjCCategories(); err == nil {
slices.SortStableFunc(cats, func(a, b objc.Category) int {
return cmp.Compare(a.Name, b.Name)
})
for _, cat := range cats {
if o.conf.Verbose {
if o.conf.Color {
quick.Highlight(os.Stdout, o.verboseOrAddrs(&cat), "objc", "terminal256", o.conf.Theme)
quick.Highlight(os.Stdout, "\n/****************************************/\n\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(o.verboseOrAddrs(&cat))
}
} else {
if o.conf.Color {
quick.Highlight(os.Stdout, cat.String()+"\n", "objc", "terminal256", o.conf.Theme)
} else {
fmt.Println(cat.String())
}
}
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
if o.conf.ObjcRefs {
if protRefs, err := m.GetObjCProtoReferences(); err == nil {
fmt.Printf("\n@protocol refs\n")
for off, prot := range protRefs {
fmt.Printf("0x%011x => 0x%011x: %s\n", off, prot.Ptr, prot.Name)
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
if clsRefs, err := m.GetObjCClassReferences(); err == nil {
fmt.Printf("\n@class refs\n")
for off, cls := range clsRefs {
fmt.Printf("0x%011x => 0x%011x: %s\n", off, cls.ClassPtr, cls.Name)
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
if supRefs, err := m.GetObjCSuperReferences(); err == nil {
fmt.Printf("\n@super refs\n")
for off, sup := range supRefs {
fmt.Printf("0x%011x => 0x%011x: %s\n", off, sup.ClassPtr, sup.Name)
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
if selRefs, err := m.GetObjCSelectorReferences(); err == nil {
fmt.Printf("\n@selectors refs\n")
for off, sel := range selRefs {
fmt.Printf("0x%011x => 0x%011x: %s\n", off, sel.VMAddr, sel.Name)
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
if o.conf.Verbose {
if classes, err := m.GetObjCClassNames(); err == nil {
fmt.Printf("\n@objc_classname\n")
for vmaddr, className := range classes {
fmt.Printf("0x%011x: %s\n", vmaddr, className)
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
if methods, err := m.GetObjCMethodNames(); err == nil {
fmt.Printf("\n@objc_methname\n")
for vmaddr, method := range methods {
fmt.Printf("0x%011x: %s\n", vmaddr, method)
}
} else if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
}
}
}
return nil
}
// Headers outputs ObjC class-dump headers from a MachO
func (o *ObjC) Headers() error {
// scan DSC for Foundation/CoreFoundation classes and protocols
if err := o.scanBaseFrameworks(); err != nil {
return err
}
writeHeaders := func(m *macho.File) error {
var headers []string
if !m.HasObjC() {
return nil
}
if id := m.DylibID(); id != nil {
o.conf.Name = filepath.Base(id.Name)
}
var buildVersions []string
if bvers := m.GetLoadsByName("LC_BUILD_VERSION"); len(bvers) > 0 {
for _, bv := range bvers {
buildVersions = append(buildVersions, bv.String())
}
}
var sourceVersion string
if svers := m.GetLoadsByName("LC_SOURCE_VERSION"); len(svers) > 0 {
sourceVersion = svers[0].String()
}
imps, err := o.processForwardDeclarations(m)
if err != nil {
return err
}
/* generate ObjC class headers */
classes, err := m.GetObjCClasses()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
}
slices.SortStableFunc(classes, func(a, b objc.Class) int {
return cmp.Compare(a.Name, b.Name)
})
for _, class := range classes {
var props []string
var setters []string
for _, prop := range class.Props {
props = append(props, prop.Name)
setters = append(setters, "set"+strings.ToUpper(prop.Name[:1])+prop.Name[1:]+":")
}
slices.Sort(props)
slices.Sort(setters)
// remove ivars that are properties
class.Ivars = slices.DeleteFunc(class.Ivars, func(i objc.Ivar) bool {
// return slices.Contains(props, i.Name) || slices.Contains(props, strings.TrimPrefix(i.Name, "_")) TODO: use this instead
return slices.Contains(props, strings.TrimPrefix(i.Name, "_"))
})
// remove methods that are property getter/setter
class.InstanceMethods = slices.DeleteFunc(class.InstanceMethods, func(m objc.Method) bool {
return slices.Contains(props, m.Name) || slices.Contains(setters, m.Name)
})
fname := filepath.Join(o.conf.Output, o.conf.Name, class.Name+".h")
if err := writeHeader(&headerInfo{
FileName: fname,
IpswVersion: o.conf.IpswVersion,
BuildVersions: buildVersions,
SourceVersion: sourceVersion,
Name: class.Name,
Imports: imps[class.Name],
Object: o.verboseOrAddrs(&class),
}); err != nil {
return err
}
headers = append(headers, filepath.Base(fname))
}
/* generate ObjC protocol headers */
protos, err := m.GetObjCProtocols()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
}
slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
return cmp.Compare(a.Name, b.Name)
})
seen := make(map[uint64]bool)
for _, proto := range protos {
if !slices.Contains(baseFrameworks, o.conf.Name) {
if _, found := slices.BinarySearch(o.baseFWs["protocols"], proto.Name); found {
continue // skip Foundation protocols
}
}
if _, ok := seen[proto.Ptr]; !ok { // prevent displaying duplicates
var props []string
var setters []string
for _, prop := range proto.InstanceProperties {
props = append(props, prop.Name)
setters = append(setters, "set"+strings.ToUpper(prop.Name[:1])+prop.Name[1:]+":")
}
slices.Sort(props)
slices.Sort(setters)
// remove methods that are property getter/setter
proto.InstanceMethods = slices.DeleteFunc(proto.InstanceMethods, func(m objc.Method) bool {
return slices.Contains(props, m.Name) || slices.Contains(setters, m.Name)
})
proto.OptionalInstanceMethods = slices.DeleteFunc(proto.OptionalInstanceMethods, func(m objc.Method) bool {
return slices.Contains(props, m.Name) || slices.Contains(setters, m.Name)
})
fname := filepath.Join(o.conf.Output, o.conf.Name, proto.Name+"-Protocol.h")
if err := writeHeader(&headerInfo{
FileName: fname,
IpswVersion: o.conf.IpswVersion,
BuildVersions: buildVersions,
SourceVersion: sourceVersion,
Name: proto.Name + "_Protocol",
Imports: imps[proto.Name+"-Protocol"],
Object: o.verboseOrAddrs(&proto),
}); err != nil {
return err
}
headers = append(headers, filepath.Base(fname))
seen[proto.Ptr] = true
}
}
/* generate ObjC category headers */
cats, err := m.GetObjCCategories()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
}
slices.SortStableFunc(cats, func(a, b objc.Category) int {
return cmp.Compare(a.Name, b.Name)
})
for _, cat := range cats {
fname := filepath.Join(o.conf.Output, o.conf.Name, cat.Name+".h")
if cat.Class != nil && cat.Class.Name != "" {
fname = filepath.Join(o.conf.Output, o.conf.Name, cat.Class.Name+"+"+cat.Name+".h")
}
var name string
if cat.Class != nil && cat.Class.Name != "" {
name = cat.Class.Name + "_" + cat.Name
} else {
name = cat.Name
}
if err := writeHeader(&headerInfo{
FileName: fname,
IpswVersion: o.conf.IpswVersion,
BuildVersions: buildVersions,
SourceVersion: sourceVersion,
Name: name,
Imports: imps[cat.Name],
Object: o.verboseOrAddrs(&cat),
}); err != nil {
return err
}
headers = append(headers, filepath.Base(fname))
}
/* generate umbrella header */
if len(headers) > 0 {
var umbrella string
if slices.Contains(headers, o.conf.Name+".h") {
umbrella = o.conf.Name + "-Umbrella"
} else {
umbrella = o.conf.Name
}
for i, header := range headers {
headers[i] = "#import \"" + header + "\""
}
fname := filepath.Join(o.conf.Output, o.conf.Name, umbrella+".h")
if err := writeHeader(&headerInfo{
FileName: fname,
IpswVersion: o.conf.IpswVersion,
BuildVersions: buildVersions,
SourceVersion: sourceVersion,
IsUmbrella: true,
Name: strings.ReplaceAll(umbrella, "-", "_"),
Object: strings.Join(headers, "\n") + "\n",
}); err != nil {
return err
}
}
return nil
}
if len(o.deps) > 0 {
for _, m := range o.deps {
if err := writeHeaders(m); err != nil {
return err
}
}
}
return writeHeaders(o.file)
}
type XCFrameworkAvailableLibrary struct {
BinaryPath string `plist:"BinaryPath"`
LibraryIdentifier string `plist:"LibraryIdentifier"`
LibraryPath string `plist:"libraryPath"`
SupportedArchitectures []string `plist:"SupportedArchitectures"`
SupportedPlatform string `plist:"SupportedPlatform"`
SupportedPlatformVariant string `plist:"SupportedPlatformVariant,omitempty"`
}
type XCFrameworkInfoPlist struct {
AvailableLibraries []XCFrameworkAvailableLibrary `plist:"AvailableLibraries"`
CFBundlePackageType string `plist:"CFBundlePackageType"`
XCFrameworkFormatVersion string `plist:"XCFrameworkFormatVersion"`
}
type XCFrameworkLibraryInfoPlist struct {
BuildMachineOSBuild string `plist:"BuildMachineOSBuild"`
CFBundleDevelopmentRegion string `plist:"CFBundleDevelopmentRegion"`
CFBundleExecutable string `plist:"CFBundleExecutable"`
CFBundleIdentifier string `plist:"CFBundleIdentifier"`
CFBundleInfoDictionaryVersion string `plist:"CFBundleInfoDictionaryVersion"`
CFBundleName string `plist:"CFBundleName"`
CFBundlePackageType string `plist:"CFBundlePackageType"`
CFBundleShortVersionString string `plist:"CFBundleShortVersionString"`
CFBundleSignature string `plist:"CFBundleSignature"`
CFBundleSupportedPlatforms []string `plist:"CFBundleSupportedPlatforms"`
CFBundleVersion string `plist:"CFBundleVersion"`
DTCompiler string `plist:"DTCompiler"`
DTPlatformBuild string `plist:"DTPlatformBuild"`
DTPlatformName string `plist:"DTPlatformName"`
DTPlatformVersion string `plist:"DTPlatformVersion"`
DTSDKBuild string `plist:"DTSDKBuild"`
DTSDKName string `plist:"DTSDKName"`
DTXcode string `plist:"DTXcode"`
DTXcodeBuild string `plist:"DTXcodeBuild"`
LSMinimumSystemVersion string `plist:"LSMinimumSystemVersion,omitempty"`
MinimumOSVersion string `plist:"MinimumOSVersion,omitempty"`
UIDeviceFamily []uint64 `plist:"UIDeviceFamily,omitempty"`
}
type XCFrameworkConfig struct {
LibraryIdentifier string
SupportedArchitectures []string
SupportedPlatform string
SupportedPlatformVariant string
CFBundleVersion string
DTPlatformVersion string
LSMinimumSystemVersion string
}
// XCFramework outputs and XCFramework for a DSC dylib
func (o *ObjC) XCFramework() error {
var xcfw XCFrameworkConfig
xcfolder := filepath.Join(o.conf.Output, o.conf.Name+".xcframework")
if err := os.MkdirAll(xcfolder, 0o750); err != nil {
return fmt.Errorf("failed to create XCFramework folder: %w", err)
}
image, err := o.cache.Image(o.conf.Name)
if err != nil {
return fmt.Errorf("failed to get image %s: %w", o.conf.Name, err)
}
m, err := image.GetMacho()
if err != nil {
return fmt.Errorf("failed to get macho from image %s: %w", o.conf.Name, err)
}
if bvs := m.BuildVersions(); len(bvs) == 0 { // TODO: support universal MachOs (with multiple architectures)
return fmt.Errorf("no build versions found in %s", o.conf.Name)
} else {
for _, bv := range bvs {
switch bv.Platform {
case types.Platform_iOsSimulator, types.Platform_tvOsSimulator, types.Platform_watchOsSimulator, types.Platform_visionOsSimulator:
xcfw.LibraryIdentifier += "_simulator"
xcfw.SupportedPlatformVariant = "simulator"
default:
xcfw.LibraryIdentifier += strings.ToLower(bv.Platform.String())
xcfw.SupportedPlatform = strings.ToLower(bv.Platform.String())
xcfw.DTPlatformVersion = bv.Sdk.String()
xcfw.LSMinimumSystemVersion = bv.Minos.String()
switch m.CPU {
case types.CPUAmd64:
xcfw.LibraryIdentifier += "_x86_64"
xcfw.SupportedArchitectures = append(xcfw.SupportedArchitectures, "x86_64")
case types.CPUArm64:
if m.SubCPU.String(m.CPU) == "arm64e" {
xcfw.LibraryIdentifier += "_arm64e"
} else {
xcfw.LibraryIdentifier += "_arm64"
}
}
}
}
}
if id := m.DylibID(); id != nil {
xcfw.CFBundleVersion = id.CurrentVersion.String()
}
/* generate XCFramework Info.plist */
f, err := os.Create(filepath.Join(xcfolder, "Info.plist"))
if err != nil {
return fmt.Errorf("failed to create %s: %w", filepath.Join(xcfolder, "Info.plist"), err)
}
defer f.Close()
enc := plist.NewEncoder(f)
enc.Indent(" ")
if err := enc.Encode(XCFrameworkInfoPlist{
AvailableLibraries: []XCFrameworkAvailableLibrary{
{
BinaryPath: o.conf.Name + ".framework/" + o.conf.Name + ".tbd",
LibraryIdentifier: xcfw.LibraryIdentifier,
LibraryPath: o.conf.Name + ".framework",
SupportedArchitectures: xcfw.SupportedArchitectures,
SupportedPlatform: xcfw.SupportedPlatform,
SupportedPlatformVariant: xcfw.SupportedPlatformVariant,
},
},
CFBundlePackageType: "XFWK",
XCFrameworkFormatVersion: "1.0",
}); err != nil {
return fmt.Errorf("failed to create XCFramework Info.plist")
}
/* create folder structure */
fwfolder := filepath.Join(xcfolder, xcfw.LibraryIdentifier, o.conf.Name+".framework")
if err := os.MkdirAll(filepath.Join(fwfolder, "Headers"), 0o750); err != nil {
return fmt.Errorf("failed to create Headers folder: %w", err)
}
if err := os.MkdirAll(filepath.Join(fwfolder, "Modules"), 0o750); err != nil {
return fmt.Errorf("failed to create Modules folder: %w", err)
}
/* generate framework tbd */
var reexports []string
if rexps := m.GetLoadsByName("LC_REEXPORT_DYLIB"); len(rexps) > 0 {
for _, rexp := range rexps {
reexports = append(reexports, rexp.(*macho.ReExportDylib).Name)
}
}
t, err := tbd.NewTBD(image, reexports, false)
if err != nil {
return fmt.Errorf("failed to create tbd: %w", err)
}
outTBD, err := t.Generate()
if err != nil {
return fmt.Errorf("failed to generate tbd: %w", err)
}
outTBD += "...\n"
tbdFile := filepath.Join(fwfolder, o.conf.Name+".tbd")
if err = os.WriteFile(tbdFile, []byte(outTBD), 0o660); err != nil {
return fmt.Errorf("failed to write tbd file %s: %v", tbdFile, err)
}
/* generate modulemap */
if err := os.WriteFile(filepath.Join(fwfolder, "Modules", "module.modulemap"), fmt.Appendf(nil,
"module %s [system] {\n"+
" header \"Headers/%s.h\"\n"+ // NOTE: this SHOULD be the umbrella header
" export *\n"+
"}\n", o.conf.Name, o.conf.Name,
), 0o660); err != nil {
return fmt.Errorf("failed to write module.modulemap file: %v", err)
}
/* generate XCFramework Library Info.plist */
f2, err := os.Create(filepath.Join(fwfolder, "Info.plist"))
if err != nil {
return fmt.Errorf("failed to create %s: %w", filepath.Join(fwfolder, "Info.plist"), err)
}
defer f2.Close()
enc = plist.NewEncoder(f2)
enc.Indent(" ")
if err := enc.Encode(XCFrameworkLibraryInfoPlist{
BuildMachineOSBuild: "23E224",
CFBundleDevelopmentRegion: "en",
CFBundleExecutable: o.conf.Name + ".tbd",
CFBundleIdentifier: "com.apple." + strings.ToLower(o.conf.Name),
CFBundleInfoDictionaryVersion: "6.0",
CFBundleName: o.conf.Name,
CFBundlePackageType: "FMWK",
CFBundleShortVersionString: "1.0",
CFBundleSignature: "????",
CFBundleVersion: xcfw.CFBundleVersion,
DTCompiler: "com.apple.compilers.llvm.clang.1_0",
CFBundleSupportedPlatforms: []string{xcfw.SupportedPlatform}, // TODO: add variants (or universal dylib/macho support)
DTPlatformName: xcfw.SupportedPlatform,
DTPlatformVersion: xcfw.DTPlatformVersion,
DTPlatformBuild: "",
DTSDKBuild: "23E224",
DTSDKName: fmt.Sprintf("%s%s.internal", xcfw.SupportedPlatform, xcfw.DTPlatformVersion),
DTXcode: "1500",
DTXcodeBuild: "15E6079e", // Xcode 15.3
LSMinimumSystemVersion: xcfw.LSMinimumSystemVersion,
}); err != nil {
return fmt.Errorf("failed to create XCFramework Info.plist")
}
/* generate Headers */
o.conf.Headers = true
o.conf.Output = filepath.Join(fwfolder, "Headers")
return o.Headers()
}
func (o *ObjC) SwiftPackage() error {
return fmt.Errorf("not implemented yet (coming soon)")
}
/* utils */
func writeHeader(hdr *headerInfo) error {
var out strings.Builder
out.WriteString(fmt.Sprintf(
"//\n"+
"// Generated by https://github.com/blacktop/ipsw (%s)\n"+
"//\n"+
"// - LC_BUILD_VERSION: %s\n"+
"// - LC_SOURCE_VERSION: %s\n"+
"//\n"+
"#ifndef %s_h\n"+
"#define %s_h\n",
hdr.IpswVersion,
strings.Join(hdr.BuildVersions, "\n// - LC_BUILD_VERSION: "),
hdr.SourceVersion,
hdr.Name,
hdr.Name))
if !hdr.IsUmbrella {
out.WriteString("@import Foundation;\n")
}
out.WriteString("\n")
if len(hdr.Imports.Imports) > 0 {
for _, imp := range hdr.Imports.Imports {
out.WriteString(fmt.Sprintf("#include \"%s\"\n", imp))
}
}
if len(hdr.Imports.Locals) > 0 {
for _, local := range hdr.Imports.Locals {
out.WriteString(fmt.Sprintf("#include \"%s\"\n", local))
}
}
if len(hdr.Imports.Imports) > 0 || len(hdr.Imports.Locals) > 0 {
out.WriteString("\n")
}
if len(hdr.Imports.Classes) > 0 {
out.WriteString(fmt.Sprintf("@class %s;\n", strings.Join(hdr.Imports.Classes, ", ")))
}
if len(hdr.Imports.Protos) > 0 {
out.WriteString(fmt.Sprintf("@protocol %s;\n", strings.Join(hdr.Imports.Protos, ", ")))
}
if len(hdr.Imports.Classes) > 0 || len(hdr.Imports.Protos) > 0 {
out.WriteString("\n")
}
out.WriteString(fmt.Sprintf("%s\n", hdr.Object))
out.WriteString(fmt.Sprintf("#endif /* %s_h */\n", hdr.Name))
if err := os.MkdirAll(filepath.Dir(hdr.FileName), 0o750); err != nil {
return err
}
log.Infof("Creating %s", hdr.FileName)
if err := os.WriteFile(hdr.FileName, []byte(out.String()), 0644); err != nil {
if pe, ok := err.(*os.PathError); ok {
if pe.Err == syscall.ENAMETOOLONG {
base := filepath.Base(strings.TrimSuffix(hdr.FileName, filepath.Ext(hdr.FileName)))
hdr.FileName = filepath.Join(filepath.Dir(hdr.FileName), base[:50]+".h")
log.Warnf("Filename too long; truncating to '%s'", hdr.FileName)
if err := os.WriteFile(hdr.FileName, []byte(out.String()), 0644); err != nil {
return fmt.Errorf("failed to write header %s: %v", hdr.FileName, err)
}
}
}
}
return nil
}
func (o *ObjC) processForwardDeclarations(m *macho.File) (map[string]Imports, error) {
var classNames []string
var protoNames []string
imps := make(map[string]Imports)
classes, err := m.GetObjCClasses()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return nil, err
}
}
slices.SortStableFunc(classes, func(a, b objc.Class) int {
return cmp.Compare(a.Name, b.Name)
})
for _, class := range classes {
classNames = append(classNames, class.Name)
}
protos, err := m.GetObjCProtocols()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return nil, err
}
}
slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
return cmp.Compare(a.Name, b.Name)
})
for _, proto := range protos {
protoNames = append(protoNames, proto.Name)
}
for _, class := range classes {
var imp Imports
if superClass := class.SuperClass; superClass != "" {
if slices.Contains(classNames, superClass) {
imp.Locals = append(imp.Locals, superClass+".h")
} else {
imp.Classes = append(imp.Classes, superClass)
}
}
for _, prot := range class.Protocols {
if slices.Contains(protoNames, prot.Name) {
imp.Locals = append(imp.Locals, prot.Name+"-Protocol.h")
} else {
imp.Protos = append(imp.Protos, prot.Name)
}
}
for _, ivar := range class.Ivars {
typ, _ := strings.CutSuffix(ivar.Verbose(), ivar.Name+";")
if err := o.fillImportsForType(typ, class.Name, "", classNames, protoNames, &imp); err != nil {
return nil, err
}
}
for _, prop := range class.Props {
typ := prop.Type()
if err := o.fillImportsForType(typ, class.Name, "", classNames, protoNames, &imp); err != nil {
return nil, err
}
}
for _, method := range class.InstanceMethods {
if method.Types == "" {
log.Warnf("Instance method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, class.Name, method.TypesVMAddr)
continue
}
for i := 0; i < method.NumberOfArguments(); i++ {
typ := method.ArgumentType(i)
if err := o.fillImportsForType(typ, class.Name, "", classNames, protoNames, &imp); err != nil {
return nil, err
}
if i == 0 {
i += 2
}
}
}
for _, method := range class.ClassMethods {
if method.Types == "" {
log.Warnf("Class method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, class.Name, method.TypesVMAddr)
continue
}
for i := 0; i < method.NumberOfArguments(); i++ {
typ := method.ArgumentType(i)
if err := o.fillImportsForType(typ, class.Name, "", classNames, protoNames, &imp); err != nil {
return nil, err
}
if i == 0 {
i += 2
}
}
}
imp.uniq(o.baseFWs)
imps[class.Name] = imp
}
for _, proto := range protos {
var imp Imports
for _, prot := range proto.Prots {
if slices.Contains(protoNames, prot.Name) {
imp.Locals = append(imp.Locals, prot.Name+"-Protocol.h")
} else {
imp.Protos = append(imp.Protos, prot.Name)
}
}
for _, prop := range proto.InstanceProperties {
typ := prop.Type()
if err := o.fillImportsForType(typ, "", proto.Name, classNames, protoNames, &imp); err != nil {
return nil, err
}
}
for _, method := range proto.InstanceMethods {
if method.Types == "" {
log.Warnf("Protocol instance method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
continue
}
for i := 0; i < method.NumberOfArguments(); i++ {
typ := method.ArgumentType(i)
if err := o.fillImportsForType(typ, "", proto.Name, classNames, protoNames, &imp); err != nil {
return nil, err
}
if i == 0 {
i += 2
}
}
}
for _, method := range proto.ClassMethods {
if method.Types == "" {
log.Warnf("Protocol class method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
continue
}
for i := 0; i < method.NumberOfArguments(); i++ {
typ := method.ArgumentType(i)
if err := o.fillImportsForType(typ, "", proto.Name, classNames, protoNames, &imp); err != nil {
return nil, err
}
if i == 0 {
i += 2
}
}
}
for _, method := range proto.OptionalInstanceMethods {
if method.Types == "" {
log.Warnf("Protocol optional instance method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
continue
}
for i := 0; i < method.NumberOfArguments(); i++ {
typ := method.ArgumentType(i)
if err := o.fillImportsForType(typ, "", proto.Name, classNames, protoNames, &imp); err != nil {
return nil, err
}
if i == 0 {
i += 2
}
}
}
for _, method := range proto.OptionalClassMethods {
if method.Types == "" {
log.Warnf("Protocol optional class method %s in %s has empty type encoding (TypesVMAddr=%#x)", method.Name, proto.Name, method.TypesVMAddr)
continue
}
for i := 0; i < method.NumberOfArguments(); i++ {
typ := method.ArgumentType(i)
if err := o.fillImportsForType(typ, "", proto.Name, classNames, protoNames, &imp); err != nil {
return nil, err
}
if i == 0 {
i += 2
}
}
}
imp.uniq(o.baseFWs)
imps[proto.Name+"-Protocol"] = imp
}
return imps, nil
}
func (o *ObjC) scanBaseFrameworks() error {
o.baseFWs["classes"] = []string{}
o.baseFWs["protocols"] = []string{}
if o.cache != nil {
for _, name := range baseFrameworks {
img, err := o.cache.Image(name)
if err != nil {
return err
}
m, err := img.GetMacho()
if err != nil {
return err
}
classes, err := m.GetObjCClasses()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
}
slices.SortStableFunc(classes, func(a, b objc.Class) int {
return cmp.Compare(a.Name, b.Name)
})
for _, class := range classes {
o.baseFWs["classes"] = append(o.baseFWs["classes"], class.Name)
}
protos, err := m.GetObjCProtocols()
if err != nil {
if !errors.Is(err, macho.ErrObjcSectionNotFound) {
return err
}
}
slices.SortStableFunc(protos, func(a, b objc.Protocol) int {
return cmp.Compare(a.Name, b.Name)
})
for _, proto := range protos {
o.baseFWs["protocols"] = append(o.baseFWs["protocols"], proto.Name)
}
slices.Sort(o.baseFWs["classes"])
slices.Sort(o.baseFWs["protocols"])
}
}
return nil
}
func (o *ObjC) fillImportsForType(typ string, className string, protoName string, classNames []string, protoNames []string, imp *Imports) error {
typ = strings.Trim(typ, ` *`)
if typ == "" {
return nil
}
// Check for outermost braces first
if hasOutermostDelimiters(typ, "{", "}") {
lTyp, mTyps, _, err := o.contextualSplit(typ, "{", ";", "}")
if err != nil {
return err
}
if err := o.fillImportsForType(lTyp, className, protoName, classNames, protoNames, imp); err != nil {
return err
}
for _, mTyp := range mTyps {
mTyp = strings.ReplaceAll(mTyp, `&`, ``)
if err := o.fillImportsForType(mTyp, className, protoName, classNames, protoNames, imp); err != nil {
return err
}
}
return nil
}
// Check for outermost angle brackets
if hasOutermostDelimiters(typ, "<", ">") {
lTyp, mTyps, _, err := o.contextualSplit(typ, "<", ",", ">")
if err != nil {
return err
}
if err := o.fillImportsForType(lTyp, className, protoName, classNames, protoNames, imp); err != nil {
return err
}
if strings.HasPrefix(lTyp, "struct ") || strings.Contains(lTyp, "::") {
// C++ Template
for _, mTyp := range mTyps {
if err := o.fillImportsForType(mTyp, className, protoName, classNames, protoNames, imp); err != nil {
return err
}
}
} else {
// Objective-C Qualifier
for _, mTyp := range mTyps {
mTyp := strings.Trim(mTyp, ` *`)
if mTyp == "" {
continue
}
if !slices.Contains(protoNames, mTyp) {
imp.Protos = append(imp.Protos, mTyp)
continue
}
if mTyp != protoName {
imp.Locals = append(imp.Locals, mTyp+"-Protocol.h")
}
}
}
return nil
}
typ = o.nonBuiltInType(typ)
if typ == "" {
return nil
}
if !slices.Contains(classNames, typ) {
imp.Classes = append(imp.Classes, typ)
return nil
}
if typ != className {
imp.Locals = append(imp.Locals, typ+".h")
}
return nil
}
func (o *ObjC) nonBuiltInType(typ string) string {
if typ == "" {
return ""
}
if strings.ContainsAny(typ, "()[]{}:;") {
return ""
}
switch before, after, _ := strings.Cut(typ, " "); before {
case "_Bool", "_Complex", "_Imaginary", "BOOL", "Class", "IMP", "Ivar", "Method", "SEL", "bool", "char", "class", "double", "enum", "float", "id", "int", "long", "short", "signed", "struct", "union", "unsigned", "void":
return ""
case "_Atomic", "bycopy", "byref", "const", "in", "inout", "oneway", "out", "restrict", "volatile":
return o.nonBuiltInType(after)
}
if idx := strings.Index(typ, "*"); idx != -1 {
typ = typ[:idx]
}
return strings.TrimSpace(typ)
}
func (o *ObjC) contextualSplit(typ string, lDelim string, mDelim string, rDelim string) (string, []string, string, error) {
// Find the outermost (last) left delimiter by scanning backwards
lDelimIdx := -1
level := 0
// Scan backwards to find the outermost left delimiter
for i := len(typ) - 1; i >= 0; i-- {
char := string(typ[i])
// Handle closing delimiters (going backwards)
if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
level++
}
// Handle opening delimiters (going backwards)
if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
level--
// If we're at level 0 and found our target left delimiter, this is the outermost one
if level == 0 && char == lDelim {
lDelimIdx = i
break
}
}
}
if lDelimIdx == -1 {
return "", nil, "", errors.New("contextualSplit failed: left delimiter not found")
}
// Now find the matching right delimiter going forwards from the left delimiter
rDelimIdx := -1
level = 0
for i := lDelimIdx; i < len(typ); i++ {
char := string(typ[i])
// Handle opening delimiters
if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
level++
}
// Handle closing delimiters
if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
level--
// If we're back to level 0 and this is our target right delimiter, we found the match
if level == 0 && char == rDelim {
rDelimIdx = i
break
}
}
}
if rDelimIdx == -1 {
return "", nil, "", errors.New("contextualSplit failed: matching right delimiter not found")
}
l := typ[:lDelimIdx]
m := typ[lDelimIdx+1 : rDelimIdx]
r := typ[rDelimIdx+1:]
// Split the middle part by mDelim, respecting nested delimiters
var typs []string
level = 0
start := 0
for i := 0; i < len(m); i++ {
char := string(m[i])
// Track nesting level for all bracket types
if char == "<" || char == "{" || char == "(" || char == "[" {
level++
} else if char == ">" || char == "}" || char == ")" || char == "]" {
level--
} else if level == 0 && char == mDelim {
// Only split on mDelim when we're at the top level (not inside brackets)
typs = append(typs, strings.TrimSpace(m[start:i]))
start = i + 1
}
}
// Add the last part
if start < len(m) {
typs = append(typs, strings.TrimSpace(m[start:]))
}
return l, typs, r, nil
}
// hasOutermostDelimiters checks if the given delimiters exist at the outermost level
// (not nested inside other delimiters)
func hasOutermostDelimiters(typ string, lDelim string, rDelim string) bool {
lDelimIdx := -1
level := 0
// Scan backwards to find the outermost left delimiter
for i := len(typ) - 1; i >= 0; i-- {
char := string(typ[i])
// Handle closing delimiters (going backwards)
if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
level++
}
// Handle opening delimiters (going backwards)
if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
level--
// If we're at level 0 and found our target left delimiter, this is the outermost one
if level == 0 && char == lDelim {
lDelimIdx = i
break
}
}
}
if lDelimIdx == -1 {
return false
}
// Now check if there's a matching right delimiter
level = 0
for i := lDelimIdx; i < len(typ); i++ {
char := string(typ[i])
// Handle opening delimiters
if char == lDelim || char == "<" || char == "{" || char == "(" || char == "[" {
level++
}
// Handle closing delimiters
if char == rDelim || char == ">" || char == "}" || char == ")" || char == "]" {
level--
// If we're back to level 0 and this is our target right delimiter, we found the match
if level == 0 && char == rDelim {
return true
}
}
}
return false
}