mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
1371 lines
40 KiB
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
|
|
}
|