ipeddle 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ipeddle/macho.py ADDED
@@ -0,0 +1,281 @@
1
+ """A small, dependency-free Mach-O reader (enough for ``ipeddle inspect``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ from dataclasses import dataclass, field
7
+
8
+ MH_MAGIC_64 = 0xFEEDFACF
9
+ FAT_MAGIC = 0xCAFEBABE
10
+ FAT_MAGIC_64 = 0xCAFEBABF
11
+
12
+ CPU_TYPE_ARM64 = 0x0100000C
13
+ CPU_TYPE_X86_64 = 0x01000007
14
+ CPU_SUBTYPE_ARM64E = 2
15
+
16
+ LC_REQ_DYLD = 0x80000000
17
+ LC_SEGMENT_64 = 0x19
18
+ LC_LOAD_DYLIB = 0xC
19
+ LC_LOAD_WEAK_DYLIB = 0x18 | LC_REQ_DYLD
20
+ LC_REEXPORT_DYLIB = 0x1F | LC_REQ_DYLD
21
+ LC_LAZY_LOAD_DYLIB = 0x20
22
+ LC_LOAD_UPWARD_DYLIB = 0x23 | LC_REQ_DYLD
23
+ LC_ID_DYLIB = 0xD
24
+ LC_RPATH = 0x1C | LC_REQ_DYLD
25
+ LC_ENCRYPTION_INFO_64 = 0x2C
26
+ LC_VERSION_MIN_IPHONEOS = 0x25
27
+ LC_BUILD_VERSION = 0x32
28
+ LC_DYLD_INFO = 0x22
29
+ LC_DYLD_INFO_ONLY = 0x22 | LC_REQ_DYLD
30
+ LC_DYLD_CHAINED_FIXUPS = 0x34 | LC_REQ_DYLD
31
+ LC_CODE_SIGNATURE = 0x1D
32
+ LC_MAIN = 0x28 | LC_REQ_DYLD
33
+
34
+ DYLIB_CMDS = {LC_LOAD_DYLIB: "load", LC_LOAD_WEAK_DYLIB: "weak", LC_REEXPORT_DYLIB: "reexport",
35
+ LC_LAZY_LOAD_DYLIB: "lazy", LC_LOAD_UPWARD_DYLIB: "upward"}
36
+
37
+ PLATFORMS = {1: "macOS", 2: "iOS", 3: "tvOS", 4: "watchOS", 5: "bridgeOS", 6: "Mac Catalyst",
38
+ 7: "iOS Simulator", 8: "tvOS Simulator", 9: "watchOS Simulator", 10: "DriverKit",
39
+ 11: "visionOS", 12: "visionOS Simulator"}
40
+
41
+ BIND_SPECIAL_SELF = 0
42
+ BIND_SPECIAL_MAIN = -1
43
+ BIND_SPECIAL_FLAT = -2
44
+ BIND_SPECIAL_WEAK = -3
45
+
46
+
47
+ class MachOError(Exception):
48
+ pass
49
+
50
+
51
+ @dataclass
52
+ class Dylib:
53
+ path: str
54
+ kind: str
55
+
56
+
57
+ @dataclass
58
+ class Import:
59
+ name: str
60
+ ordinal: int # 1-based library ordinal, or a BIND_SPECIAL_* value
61
+ weak: bool = False
62
+
63
+
64
+ @dataclass
65
+ class Slice:
66
+ cputype: int
67
+ cpusubtype: int
68
+ filetype: int
69
+ platform: str | None = None
70
+ minos: str | None = None
71
+ sdk: str | None = None
72
+ dylibs: list[Dylib] = field(default_factory=list)
73
+ rpaths: list[str] = field(default_factory=list)
74
+ imports: list[Import] = field(default_factory=list)
75
+ encrypted: bool = False
76
+ signed: bool = False
77
+ has_main: bool = False
78
+
79
+ @property
80
+ def arch(self) -> str:
81
+ if self.cputype == CPU_TYPE_ARM64:
82
+ return "arm64e" if (self.cpusubtype & 0xFF) == CPU_SUBTYPE_ARM64E else "arm64"
83
+ if self.cputype == CPU_TYPE_X86_64:
84
+ return "x86_64"
85
+ return hex(self.cputype)
86
+
87
+ def library_for(self, imp: Import) -> str:
88
+ if imp.ordinal == BIND_SPECIAL_FLAT:
89
+ return "<flat lookup>"
90
+ if imp.ordinal == BIND_SPECIAL_SELF:
91
+ return "<self>"
92
+ if imp.ordinal == BIND_SPECIAL_MAIN:
93
+ return "<main executable>"
94
+ if imp.ordinal == BIND_SPECIAL_WEAK:
95
+ return "<weak lookup>"
96
+ if 1 <= imp.ordinal <= len(self.dylibs):
97
+ return self.dylibs[imp.ordinal - 1].path
98
+ return f"<bad ordinal {imp.ordinal}>"
99
+
100
+
101
+ def _ver(v: int) -> str:
102
+ major, minor, patch = v >> 16, (v >> 8) & 0xFF, v & 0xFF
103
+ return f"{major}.{minor}" + (f".{patch}" if patch else "")
104
+
105
+
106
+ def _cstr(buf: bytes, off: int) -> str:
107
+ end = buf.find(b"\0", off)
108
+ return buf[off:end if end >= 0 else len(buf)].decode("utf-8", "replace")
109
+
110
+
111
+ def _uleb(buf: bytes, i: int) -> tuple[int, int]:
112
+ result = shift = 0
113
+ while True:
114
+ b = buf[i]
115
+ i += 1
116
+ result |= (b & 0x7F) << shift
117
+ shift += 7
118
+ if not b & 0x80:
119
+ return result, i
120
+
121
+
122
+ def _sleb(buf: bytes, i: int) -> tuple[int, int]:
123
+ result = shift = 0
124
+ while True:
125
+ b = buf[i]
126
+ i += 1
127
+ result |= (b & 0x7F) << shift
128
+ shift += 7
129
+ if not b & 0x80:
130
+ if b & 0x40:
131
+ result -= 1 << shift
132
+ return result, i
133
+
134
+
135
+ def parse_slice(data: bytes, base: int = 0) -> Slice:
136
+ magic, cputype, cpusub, filetype, ncmds, sizeofcmds, _flags, _res = struct.unpack_from("<IiiIIIII", data, base)
137
+ if magic != MH_MAGIC_64:
138
+ raise MachOError("not a 64-bit little-endian Mach-O")
139
+ sl = Slice(cputype & 0xFFFFFFFF, cpusub & 0xFFFFFFFF, filetype)
140
+ off = base + 32
141
+ end = off + sizeofcmds
142
+ chained = None
143
+ dyld_info = None
144
+ for _ in range(ncmds):
145
+ if off + 8 > end:
146
+ raise MachOError("load commands overrun their declared size")
147
+ cmd, cmdsize = struct.unpack_from("<II", data, off)
148
+ if cmdsize < 8:
149
+ raise MachOError("corrupt load command")
150
+ if cmd in DYLIB_CMDS:
151
+ name_off = struct.unpack_from("<I", data, off + 8)[0]
152
+ sl.dylibs.append(Dylib(_cstr(data, off + name_off), DYLIB_CMDS[cmd]))
153
+ elif cmd == LC_RPATH:
154
+ sl.rpaths.append(_cstr(data, off + struct.unpack_from("<I", data, off + 8)[0]))
155
+ elif cmd == LC_BUILD_VERSION:
156
+ plat, minos, sdk = struct.unpack_from("<III", data, off + 8)
157
+ sl.platform, sl.minos, sl.sdk = PLATFORMS.get(plat, str(plat)), _ver(minos), _ver(sdk)
158
+ elif cmd == LC_VERSION_MIN_IPHONEOS:
159
+ v, sdk = struct.unpack_from("<II", data, off + 8)
160
+ sl.platform, sl.minos, sl.sdk = "iOS", _ver(v), _ver(sdk)
161
+ elif cmd == LC_ENCRYPTION_INFO_64:
162
+ sl.encrypted = struct.unpack_from("<I", data, off + 16)[0] != 0
163
+ elif cmd == LC_CODE_SIGNATURE:
164
+ sl.signed = True
165
+ elif cmd == LC_MAIN:
166
+ sl.has_main = True
167
+ elif cmd == LC_DYLD_CHAINED_FIXUPS:
168
+ chained = struct.unpack_from("<II", data, off + 8)
169
+ elif cmd in (LC_DYLD_INFO, LC_DYLD_INFO_ONLY):
170
+ dyld_info = struct.unpack_from("<10I", data, off + 8)
171
+ off += cmdsize
172
+ if chained:
173
+ sl.imports = _chained_imports(data, base + chained[0], chained[1])
174
+ elif dyld_info:
175
+ imports = {}
176
+ for o, s in ((dyld_info[2], dyld_info[3]), (dyld_info[4], dyld_info[5]), (dyld_info[6], dyld_info[7])):
177
+ if s:
178
+ for imp in _bind_opcodes(data, base + o, s):
179
+ imports.setdefault((imp.name, imp.ordinal), imp)
180
+ sl.imports = list(imports.values())
181
+ return sl
182
+
183
+
184
+ def _chained_imports(data: bytes, off: int, size: int) -> list[Import]:
185
+ (_ver, _starts, imports_off, symbols_off, count, fmt, sym_fmt) = struct.unpack_from("<7I", data, off)
186
+ if sym_fmt != 0:
187
+ raise MachOError("compressed chained-fixup symbol tables are not supported")
188
+ out = []
189
+ p = off + imports_off
190
+ syms = off + symbols_off
191
+ for _ in range(count):
192
+ if fmt == 1: # DYLD_CHAINED_IMPORT
193
+ v = struct.unpack_from("<I", data, p)[0]
194
+ p += 4
195
+ lib, weak, name_off = v & 0xFF, (v >> 8) & 1, v >> 9
196
+ if lib > 0xF0:
197
+ lib -= 0x100
198
+ elif fmt == 2: # DYLD_CHAINED_IMPORT_ADDEND
199
+ v = struct.unpack_from("<I", data, p)[0]
200
+ p += 8
201
+ lib, weak, name_off = v & 0xFF, (v >> 8) & 1, v >> 9
202
+ if lib > 0xF0:
203
+ lib -= 0x100
204
+ elif fmt == 3: # DYLD_CHAINED_IMPORT_ADDEND64
205
+ v = struct.unpack_from("<Q", data, p)[0]
206
+ p += 16
207
+ lib, weak, name_off = v & 0xFFFF, (v >> 16) & 1, v >> 32
208
+ if lib > 0xFFF0:
209
+ lib -= 0x10000
210
+ else:
211
+ raise MachOError(f"unknown chained import format {fmt}")
212
+ out.append(Import(_cstr(data, syms + name_off), lib, bool(weak)))
213
+ return out
214
+
215
+
216
+ def _bind_opcodes(data: bytes, off: int, size: int) -> list[Import]:
217
+ out = []
218
+ i, end = off, off + size
219
+ ordinal, name, weak = 0, "", False
220
+ while i < end:
221
+ b = data[i]
222
+ i += 1
223
+ op, imm = b & 0xF0, b & 0x0F
224
+ if op == 0x00: # DONE (lazy streams continue)
225
+ continue
226
+ elif op == 0x10: # SET_DYLIB_ORDINAL_IMM
227
+ ordinal = imm
228
+ elif op == 0x20: # SET_DYLIB_ORDINAL_ULEB
229
+ ordinal, i = _uleb(data, i)
230
+ elif op == 0x30: # SET_DYLIB_SPECIAL_IMM
231
+ ordinal = (imm | 0xF0) - 0x100 if imm else 0
232
+ elif op == 0x40: # SET_SYMBOL_TRAILING_FLAGS_IMM
233
+ j = data.index(b"\0", i)
234
+ name = data[i:j].decode("utf-8", "replace")
235
+ weak = bool(imm & 0x1)
236
+ i = j + 1
237
+ elif op == 0x50: # SET_TYPE_IMM
238
+ pass
239
+ elif op == 0x60: # SET_ADDEND_SLEB
240
+ _, i = _sleb(data, i)
241
+ elif op == 0x70: # SET_SEGMENT_AND_OFFSET_ULEB
242
+ _, i = _uleb(data, i)
243
+ elif op == 0x80: # ADD_ADDR_ULEB
244
+ _, i = _uleb(data, i)
245
+ elif op in (0x90, 0xB0): # DO_BIND, DO_BIND_ADD_ADDR_IMM_SCALED
246
+ out.append(Import(name, ordinal, weak))
247
+ elif op == 0xA0: # DO_BIND_ADD_ADDR_ULEB
248
+ out.append(Import(name, ordinal, weak))
249
+ _, i = _uleb(data, i)
250
+ elif op == 0xC0: # DO_BIND_ULEB_TIMES_SKIPPING_ULEB
251
+ out.append(Import(name, ordinal, weak))
252
+ _, i = _uleb(data, i)
253
+ _, i = _uleb(data, i)
254
+ else:
255
+ break
256
+ return out
257
+
258
+
259
+ def parse(data: bytes) -> list[Slice]:
260
+ if len(data) < 32:
261
+ raise MachOError("file too small to be Mach-O")
262
+ magic_be = struct.unpack_from(">I", data, 0)[0]
263
+ if magic_be in (FAT_MAGIC, FAT_MAGIC_64):
264
+ n = struct.unpack_from(">I", data, 4)[0]
265
+ slices = []
266
+ for k in range(n):
267
+ if magic_be == FAT_MAGIC:
268
+ _ct, _cs, offset, _size, _align = struct.unpack_from(">IIIII", data, 8 + k * 20)
269
+ else:
270
+ _ct, _cs, offset, _size, _align, _r = struct.unpack_from(">IIQQII", data, 8 + k * 32)
271
+ slices.append(parse_slice(data, offset))
272
+ return slices
273
+ return [parse_slice(data, 0)]
274
+
275
+
276
+ def is_macho(data: bytes) -> bool:
277
+ if len(data) < 4:
278
+ return False
279
+ le = struct.unpack_from("<I", data, 0)[0]
280
+ be = struct.unpack_from(">I", data, 0)[0]
281
+ return le == MH_MAGIC_64 or be in (FAT_MAGIC, FAT_MAGIC_64)
ipeddle/mangling.py ADDED
@@ -0,0 +1,136 @@
1
+ """Tiny helpers over Swift mangled names.
2
+
3
+ We never need a full demangler: ipeddle only has to answer "which module owns
4
+ this symbol?" so it can decide which library a symbol must be bound to.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ _PREFIXES = ("_$s", "$s", "_$S", "$S")
12
+
13
+ # Kind letters that may follow a nominal type in a mangled context.
14
+ _KIND_CHARS = set("VCOPaN")
15
+
16
+
17
+ def is_swift_symbol(name: str) -> bool:
18
+ return name.startswith(_PREFIXES)
19
+
20
+
21
+ def _strip_prefix(name: str) -> str | None:
22
+ for p in _PREFIXES:
23
+ if name.startswith(p):
24
+ return name[len(p):]
25
+ return None
26
+
27
+
28
+ def _read_ident(s: str, i: int) -> tuple[str, int] | None:
29
+ """Read a ``<decimal length><identifier>`` token at ``s[i:]``.
30
+
31
+ Word-substituted identifiers (introduced by ``0``) are not decoded and
32
+ return ``None``.
33
+ """
34
+ m = re.match(r"[1-9][0-9]*", s[i:])
35
+ if not m:
36
+ return None
37
+ n = int(m.group(0))
38
+ start = i + len(m.group(0))
39
+ ident = s[start:start + n]
40
+ if len(ident) != n or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", ident):
41
+ return None
42
+ return ident, start + n
43
+
44
+
45
+ def literal_identifiers(name: str) -> list[str]:
46
+ """All literal ``<len><ident>`` tokens, in order (best effort)."""
47
+ s = _strip_prefix(name)
48
+ if s is None:
49
+ return []
50
+ out = []
51
+ i = 0
52
+ while i < len(s):
53
+ if s[i].isdigit() and s[i] != "0" and (i == 0 or not s[i - 1].isdigit()):
54
+ tok = _read_ident(s, i)
55
+ if tok:
56
+ out.append(tok[0])
57
+ i = tok[1]
58
+ continue
59
+ i += 1
60
+ return out
61
+
62
+
63
+ def owner_module(name: str, known: set[str] | frozenset[str] | None = None) -> str | None:
64
+ """Like :func:`_owner_module`; with ``known`` (the module names that exist
65
+ in the SDK), an answer that is not a module falls back to the first
66
+ module named literally in the mangling. That covers witness tables of
67
+ standard types (``$sSi7SwiftUI...WP``) and associated type descriptors
68
+ (``$s4Body7SwiftUI4ViewPTl``)."""
69
+ guess = _owner_module(name)
70
+ if known is None or guess in known:
71
+ return guess
72
+ for ident in literal_identifiers(name):
73
+ if ident in known:
74
+ return ident
75
+ return None
76
+
77
+
78
+ def _owner_module(name: str) -> str | None:
79
+ """Return the module that *defines* the entity a Swift symbol refers to.
80
+
81
+ Rules (enough for linking decisions):
82
+ * ``$s<len>Module...`` -> Module
83
+ * ``$s<context><len>ModuleE...`` (extension) -> Module of the extension
84
+ * ``$sSo<len>Type<kind><len>ModuleE...`` -> Module (extension on an
85
+ imported Objective-C/C type)
86
+ * ``$sS<x><len>ModuleE...`` -> Module (extension on a
87
+ standard library type, e.g. ``SS`` = String)
88
+ Anything else returns ``None``.
89
+ """
90
+ s = _strip_prefix(name)
91
+ if s is None or not s:
92
+ return None
93
+ i = 0
94
+ first: str | None = None
95
+ if s.startswith(("So", "SC")):
96
+ tok = _read_ident(s, 2)
97
+ if tok is None:
98
+ return None
99
+ i = tok[1]
100
+ while i < len(s) and s[i] in _KIND_CHARS:
101
+ i += 1
102
+ elif len(s) >= 2 and s[0] == "S" and s[1].isalpha():
103
+ i = 2
104
+ else:
105
+ tok = _read_ident(s, 0)
106
+ if tok is None:
107
+ return None
108
+ first, i = tok
109
+ # Walk the remaining context chain looking for an extension marker.
110
+ steps = 0
111
+ while i < len(s) and steps < 8:
112
+ steps += 1
113
+ tok = _read_ident(s, i)
114
+ if tok is None:
115
+ break
116
+ ident, j = tok
117
+ if j < len(s) and s[j] == "E":
118
+ return ident
119
+ if first is None:
120
+ # Context started with a substitution and the next identifier is
121
+ # not an extension module: we cannot tell the owner reliably.
122
+ return None
123
+ i = j
124
+ while i < len(s) and s[i] in _KIND_CHARS:
125
+ i += 1
126
+ return first
127
+
128
+
129
+ def mentions_module(name: str, module: str) -> bool:
130
+ """True if the literal ``<len><module>`` token occurs in the mangling."""
131
+ s = _strip_prefix(name) or name
132
+ tok = f"{len(module)}{module}"
133
+ for m in re.finditer(re.escape(tok), s):
134
+ if m.start() == 0 or not s[m.start() - 1].isdigit():
135
+ return True
136
+ return False