master
  1/// Non-zero for fat dylibs
  2offset: u64,
  3path: Path,
  4index: File.Index,
  5file_handle: File.HandleIndex,
  6tag: enum { dylib, tbd },
  7
  8exports: std.MultiArrayList(Export) = .{},
  9strtab: std.ArrayList(u8) = .empty,
 10id: ?Id = null,
 11ordinal: u16 = 0,
 12
 13symbols: std.ArrayList(Symbol) = .empty,
 14symbols_extra: std.ArrayList(u32) = .empty,
 15globals: std.ArrayList(MachO.SymbolResolver.Index) = .empty,
 16dependents: std.ArrayList(Id) = .empty,
 17rpaths: std.StringArrayHashMapUnmanaged(void) = .empty,
 18umbrella: File.Index,
 19platform: ?MachO.Platform = null,
 20
 21needed: bool,
 22weak: bool,
 23reexport: bool,
 24explicit: bool,
 25hoisted: bool = true,
 26referenced: bool = false,
 27
 28output_symtab_ctx: MachO.SymtabCtx = .{},
 29
 30pub fn deinit(self: *Dylib, allocator: Allocator) void {
 31    allocator.free(self.path.sub_path);
 32    self.exports.deinit(allocator);
 33    self.strtab.deinit(allocator);
 34    if (self.id) |*id| id.deinit(allocator);
 35    self.symbols.deinit(allocator);
 36    self.symbols_extra.deinit(allocator);
 37    self.globals.deinit(allocator);
 38    for (self.dependents.items) |*id| {
 39        id.deinit(allocator);
 40    }
 41    self.dependents.deinit(allocator);
 42    for (self.rpaths.keys()) |rpath| {
 43        allocator.free(rpath);
 44    }
 45    self.rpaths.deinit(allocator);
 46}
 47
 48pub fn parse(self: *Dylib, macho_file: *MachO) !void {
 49    switch (self.tag) {
 50        .tbd => try self.parseTbd(macho_file),
 51        .dylib => try self.parseBinary(macho_file),
 52    }
 53    try self.initSymbols(macho_file);
 54}
 55
 56fn parseBinary(self: *Dylib, macho_file: *MachO) !void {
 57    const tracy = trace(@src());
 58    defer tracy.end();
 59
 60    const gpa = macho_file.base.comp.gpa;
 61    const file = macho_file.getFileHandle(self.file_handle);
 62    const offset = self.offset;
 63
 64    log.debug("parsing dylib from binary: {f}", .{@as(Path, self.path)});
 65
 66    var header_buffer: [@sizeOf(macho.mach_header_64)]u8 = undefined;
 67    {
 68        const amt = try file.preadAll(&header_buffer, offset);
 69        if (amt != @sizeOf(macho.mach_header_64)) return error.InputOutput;
 70    }
 71    const header = @as(*align(1) const macho.mach_header_64, @ptrCast(&header_buffer)).*;
 72
 73    const this_cpu_arch: std.Target.Cpu.Arch = switch (header.cputype) {
 74        macho.CPU_TYPE_ARM64 => .aarch64,
 75        macho.CPU_TYPE_X86_64 => .x86_64,
 76        else => |x| {
 77            try macho_file.reportParseError2(self.index, "unknown cpu architecture: {d}", .{x});
 78            return error.InvalidMachineType;
 79        },
 80    };
 81    if (macho_file.getTarget().cpu.arch != this_cpu_arch) {
 82        try macho_file.reportParseError2(self.index, "invalid cpu architecture: {s}", .{@tagName(this_cpu_arch)});
 83        return error.InvalidMachineType;
 84    }
 85
 86    const lc_buffer = try gpa.alloc(u8, header.sizeofcmds);
 87    defer gpa.free(lc_buffer);
 88    {
 89        const amt = try file.preadAll(lc_buffer, offset + @sizeOf(macho.mach_header_64));
 90        if (amt != lc_buffer.len) return error.InputOutput;
 91    }
 92
 93    var it = LoadCommandIterator.init(&header, lc_buffer) catch |err| std.debug.panic("bad dylib: {t}", .{err});
 94    while (it.next() catch |err| std.debug.panic("bad dylib: {t}", .{err})) |cmd| switch (cmd.hdr.cmd) {
 95        .ID_DYLIB => {
 96            self.id = try Id.fromLoadCommand(gpa, cmd.cast(macho.dylib_command).?, cmd.getDylibPathName());
 97        },
 98        .REEXPORT_DYLIB => if (header.flags & macho.MH_NO_REEXPORTED_DYLIBS == 0) {
 99            const id = try Id.fromLoadCommand(gpa, cmd.cast(macho.dylib_command).?, cmd.getDylibPathName());
100            try self.dependents.append(gpa, id);
101        },
102        .DYLD_INFO_ONLY => {
103            const dyld_cmd = cmd.cast(macho.dyld_info_command).?;
104            const data = try gpa.alloc(u8, dyld_cmd.export_size);
105            defer gpa.free(data);
106            const amt = try file.preadAll(data, dyld_cmd.export_off + offset);
107            if (amt != data.len) return error.InputOutput;
108            try self.parseTrie(data, macho_file);
109        },
110        .DYLD_EXPORTS_TRIE => {
111            const ld_cmd = cmd.cast(macho.linkedit_data_command).?;
112            const data = try gpa.alloc(u8, ld_cmd.datasize);
113            defer gpa.free(data);
114            const amt = try file.preadAll(data, ld_cmd.dataoff + offset);
115            if (amt != data.len) return error.InputOutput;
116            try self.parseTrie(data, macho_file);
117        },
118        .RPATH => {
119            const path = cmd.getRpathPathName();
120            try self.rpaths.put(gpa, try gpa.dupe(u8, path), {});
121        },
122        .BUILD_VERSION,
123        .VERSION_MIN_MACOSX,
124        .VERSION_MIN_IPHONEOS,
125        .VERSION_MIN_TVOS,
126        .VERSION_MIN_WATCHOS,
127        => {
128            self.platform = MachO.Platform.fromLoadCommand(cmd);
129        },
130        else => {},
131    };
132
133    if (self.id == null) {
134        try macho_file.reportParseError2(self.index, "missing LC_ID_DYLIB load command", .{});
135        return error.MalformedDylib;
136    }
137
138    if (self.platform) |platform| {
139        if (!macho_file.platform.eqlTarget(platform)) {
140            try macho_file.reportParseError2(self.index, "invalid platform: {f}", .{
141                platform.fmtTarget(macho_file.getTarget().cpu.arch),
142            });
143            return error.InvalidTarget;
144        }
145        // TODO: this can cause the CI to fail so I'm commenting this check out so that
146        // I can work out the rest of the changes first
147        // if (macho_file.platform.version.order(platform.version) == .lt) {
148        //     try macho_file.reportParseError2(self.index, "object file built for newer platform: {f}: {f} < {f}", .{
149        //         macho_file.platform.fmtTarget(macho_file.getTarget().cpu.arch),
150        //         macho_file.platform.version,
151        //         platform.version,
152        //     });
153        //     return error.InvalidTarget;
154        // }
155    }
156}
157
158const TrieIterator = struct {
159    stream: std.Io.Reader,
160
161    fn readUleb128(it: *TrieIterator) !u64 {
162        return it.stream.takeLeb128(u64);
163    }
164
165    fn readString(it: *TrieIterator) ![:0]const u8 {
166        return it.stream.takeSentinel(0);
167    }
168
169    fn readByte(it: *TrieIterator) !u8 {
170        return it.stream.takeByte();
171    }
172};
173
174pub fn addExport(self: *Dylib, allocator: Allocator, name: []const u8, flags: Export.Flags) !void {
175    try self.exports.append(allocator, .{
176        .name = try self.addString(allocator, name),
177        .flags = flags,
178    });
179}
180
181fn parseTrieNode(
182    self: *Dylib,
183    it: *TrieIterator,
184    allocator: Allocator,
185    arena: Allocator,
186    prefix: []const u8,
187) !void {
188    const tracy = trace(@src());
189    defer tracy.end();
190    const size = try it.readUleb128();
191    if (size > 0) {
192        const flags = try it.readUleb128();
193        const kind = flags & macho.EXPORT_SYMBOL_FLAGS_KIND_MASK;
194        const out_flags = Export.Flags{
195            .abs = kind == macho.EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE,
196            .tlv = kind == macho.EXPORT_SYMBOL_FLAGS_KIND_THREAD_LOCAL,
197            .weak = flags & macho.EXPORT_SYMBOL_FLAGS_WEAK_DEFINITION != 0,
198        };
199        if (flags & macho.EXPORT_SYMBOL_FLAGS_REEXPORT != 0) {
200            _ = try it.readUleb128(); // dylib ordinal
201            const name = try it.readString();
202            try self.addExport(allocator, if (name.len > 0) name else prefix, out_flags);
203        } else if (flags & macho.EXPORT_SYMBOL_FLAGS_STUB_AND_RESOLVER != 0) {
204            _ = try it.readUleb128(); // stub offset
205            _ = try it.readUleb128(); // resolver offset
206            try self.addExport(allocator, prefix, out_flags);
207        } else {
208            _ = try it.readUleb128(); // VM offset
209            try self.addExport(allocator, prefix, out_flags);
210        }
211    }
212
213    const nedges = try it.readByte();
214
215    for (0..nedges) |_| {
216        const label = try it.readString();
217        const off = try it.readUleb128();
218        const prefix_label = try std.fmt.allocPrint(arena, "{s}{s}", .{ prefix, label });
219        const curr = it.stream.seek;
220        it.stream.seek = math.cast(usize, off) orelse return error.Overflow;
221        try self.parseTrieNode(it, allocator, arena, prefix_label);
222        it.stream.seek = curr;
223    }
224}
225
226fn parseTrie(self: *Dylib, data: []const u8, macho_file: *MachO) !void {
227    const tracy = trace(@src());
228    defer tracy.end();
229    const gpa = macho_file.base.comp.gpa;
230    var arena = std.heap.ArenaAllocator.init(gpa);
231    defer arena.deinit();
232
233    var it: TrieIterator = .{ .stream = .fixed(data) };
234    try self.parseTrieNode(&it, gpa, arena.allocator(), "");
235}
236
237fn parseTbd(self: *Dylib, macho_file: *MachO) !void {
238    const tracy = trace(@src());
239    defer tracy.end();
240
241    const gpa = macho_file.base.comp.gpa;
242
243    log.debug("parsing dylib from stub: {f}", .{self.path});
244
245    const file = macho_file.getFileHandle(self.file_handle);
246    var lib_stub = LibStub.loadFromFile(gpa, file) catch |err| {
247        try macho_file.reportParseError2(self.index, "failed to parse TBD file: {s}", .{@errorName(err)});
248        return error.MalformedTbd;
249    };
250    defer lib_stub.deinit();
251    const umbrella_lib = lib_stub.inner[0];
252
253    {
254        var id = try Id.default(gpa, umbrella_lib.installName());
255        if (umbrella_lib.currentVersion()) |version| {
256            try id.parseCurrentVersion(version);
257        }
258        if (umbrella_lib.compatibilityVersion()) |version| {
259            try id.parseCompatibilityVersion(version);
260        }
261        self.id = id;
262    }
263
264    var umbrella_libs = std.StringHashMap(void).init(gpa);
265    defer umbrella_libs.deinit();
266
267    log.debug("  (install_name '{s}')", .{umbrella_lib.installName()});
268
269    const cpu_arch = macho_file.getTarget().cpu.arch;
270    self.platform = macho_file.platform;
271
272    var matcher = try TargetMatcher.init(gpa, cpu_arch, self.platform.?.toApplePlatform());
273    defer matcher.deinit();
274
275    for (lib_stub.inner, 0..) |elem, stub_index| {
276        if (!(try matcher.matchesTargetTbd(elem))) continue;
277
278        if (stub_index > 0) {
279            // TODO I thought that we could switch on presence of `parent-umbrella` map;
280            // however, turns out `libsystem_notify.dylib` is fully reexported by `libSystem.dylib`
281            // BUT does not feature a `parent-umbrella` map as the only sublib. Apple's bug perhaps?
282            try umbrella_libs.put(elem.installName(), {});
283        }
284
285        switch (elem) {
286            .v3 => |stub| {
287                if (stub.exports) |exports| {
288                    for (exports) |exp| {
289                        if (!matcher.matchesArch(exp.archs)) continue;
290
291                        if (exp.symbols) |symbols| {
292                            for (symbols) |sym_name| {
293                                try self.addExport(gpa, sym_name, .{});
294                            }
295                        }
296
297                        if (exp.weak_symbols) |symbols| {
298                            for (symbols) |sym_name| {
299                                try self.addExport(gpa, sym_name, .{ .weak = true });
300                            }
301                        }
302
303                        if (exp.objc_classes) |objc_classes| {
304                            for (objc_classes) |class_name| {
305                                try self.addObjCClass(gpa, class_name);
306                            }
307                        }
308
309                        if (exp.objc_ivars) |objc_ivars| {
310                            for (objc_ivars) |ivar| {
311                                try self.addObjCIVar(gpa, ivar);
312                            }
313                        }
314
315                        if (exp.objc_eh_types) |objc_eh_types| {
316                            for (objc_eh_types) |eht| {
317                                try self.addObjCEhType(gpa, eht);
318                            }
319                        }
320
321                        if (exp.re_exports) |re_exports| {
322                            for (re_exports) |lib| {
323                                if (umbrella_libs.contains(lib)) continue;
324
325                                log.debug("  (found re-export '{s}')", .{lib});
326
327                                const dep_id = try Id.default(gpa, lib);
328                                try self.dependents.append(gpa, dep_id);
329                            }
330                        }
331                    }
332                }
333            },
334            .v4 => |stub| {
335                if (stub.exports) |exports| {
336                    for (exports) |exp| {
337                        if (!matcher.matchesTarget(exp.targets)) continue;
338
339                        if (exp.symbols) |symbols| {
340                            for (symbols) |sym_name| {
341                                try self.addExport(gpa, sym_name, .{});
342                            }
343                        }
344
345                        if (exp.weak_symbols) |symbols| {
346                            for (symbols) |sym_name| {
347                                try self.addExport(gpa, sym_name, .{ .weak = true });
348                            }
349                        }
350
351                        if (exp.objc_classes) |classes| {
352                            for (classes) |sym_name| {
353                                try self.addObjCClass(gpa, sym_name);
354                            }
355                        }
356
357                        if (exp.objc_ivars) |objc_ivars| {
358                            for (objc_ivars) |ivar| {
359                                try self.addObjCIVar(gpa, ivar);
360                            }
361                        }
362
363                        if (exp.objc_eh_types) |objc_eh_types| {
364                            for (objc_eh_types) |eht| {
365                                try self.addObjCEhType(gpa, eht);
366                            }
367                        }
368                    }
369                }
370
371                if (stub.reexports) |reexports| {
372                    for (reexports) |reexp| {
373                        if (!matcher.matchesTarget(reexp.targets)) continue;
374
375                        if (reexp.symbols) |symbols| {
376                            for (symbols) |sym_name| {
377                                try self.addExport(gpa, sym_name, .{});
378                            }
379                        }
380
381                        if (reexp.weak_symbols) |symbols| {
382                            for (symbols) |sym_name| {
383                                try self.addExport(gpa, sym_name, .{ .weak = true });
384                            }
385                        }
386
387                        if (reexp.objc_classes) |classes| {
388                            for (classes) |sym_name| {
389                                try self.addObjCClass(gpa, sym_name);
390                            }
391                        }
392
393                        if (reexp.objc_ivars) |objc_ivars| {
394                            for (objc_ivars) |ivar| {
395                                try self.addObjCIVar(gpa, ivar);
396                            }
397                        }
398
399                        if (reexp.objc_eh_types) |objc_eh_types| {
400                            for (objc_eh_types) |eht| {
401                                try self.addObjCEhType(gpa, eht);
402                            }
403                        }
404                    }
405                }
406
407                if (stub.objc_classes) |classes| {
408                    for (classes) |sym_name| {
409                        try self.addObjCClass(gpa, sym_name);
410                    }
411                }
412
413                if (stub.objc_ivars) |objc_ivars| {
414                    for (objc_ivars) |ivar| {
415                        try self.addObjCIVar(gpa, ivar);
416                    }
417                }
418
419                if (stub.objc_eh_types) |objc_eh_types| {
420                    for (objc_eh_types) |eht| {
421                        try self.addObjCEhType(gpa, eht);
422                    }
423                }
424            },
425        }
426    }
427
428    // For V4, we add dependent libs in a separate pass since some stubs such as libSystem include
429    // re-exports directly in the stub file.
430    for (lib_stub.inner) |elem| {
431        if (elem == .v3) continue;
432        const stub = elem.v4;
433
434        if (stub.reexported_libraries) |reexports| {
435            for (reexports) |reexp| {
436                if (!matcher.matchesTarget(reexp.targets)) continue;
437
438                for (reexp.libraries) |lib| {
439                    if (umbrella_libs.contains(lib)) continue;
440
441                    log.debug("  (found re-export '{s}')", .{lib});
442
443                    const dep_id = try Id.default(gpa, lib);
444                    try self.dependents.append(gpa, dep_id);
445                }
446            }
447        }
448    }
449}
450
451fn addObjCClass(self: *Dylib, allocator: Allocator, name: []const u8) !void {
452    try self.addObjCExport(allocator, "_OBJC_CLASS_", name);
453    try self.addObjCExport(allocator, "_OBJC_METACLASS_", name);
454}
455
456fn addObjCIVar(self: *Dylib, allocator: Allocator, name: []const u8) !void {
457    try self.addObjCExport(allocator, "_OBJC_IVAR_", name);
458}
459
460fn addObjCEhType(self: *Dylib, allocator: Allocator, name: []const u8) !void {
461    try self.addObjCExport(allocator, "_OBJC_EHTYPE_", name);
462}
463
464fn addObjCExport(
465    self: *Dylib,
466    allocator: Allocator,
467    comptime prefix: []const u8,
468    name: []const u8,
469) !void {
470    const full_name = try std.fmt.allocPrint(allocator, prefix ++ "$_{s}", .{name});
471    defer allocator.free(full_name);
472    try self.addExport(allocator, full_name, .{});
473}
474
475fn initSymbols(self: *Dylib, macho_file: *MachO) !void {
476    const gpa = macho_file.base.comp.gpa;
477
478    const nsyms = self.exports.items(.name).len;
479    try self.symbols.ensureTotalCapacityPrecise(gpa, nsyms);
480    try self.symbols_extra.ensureTotalCapacityPrecise(gpa, nsyms * @sizeOf(Symbol.Extra));
481    try self.globals.ensureTotalCapacityPrecise(gpa, nsyms);
482    self.globals.resize(gpa, nsyms) catch unreachable;
483    @memset(self.globals.items, 0);
484
485    for (self.exports.items(.name), self.exports.items(.flags)) |noff, flags| {
486        const index = self.addSymbolAssumeCapacity();
487        const symbol = &self.symbols.items[index];
488        symbol.name = noff;
489        symbol.extra = self.addSymbolExtraAssumeCapacity(.{});
490        symbol.flags.weak = flags.weak;
491        symbol.flags.tlv = flags.tlv;
492        symbol.visibility = .global;
493    }
494}
495
496pub fn resolveSymbols(self: *Dylib, macho_file: *MachO) !void {
497    const tracy = trace(@src());
498    defer tracy.end();
499
500    if (!self.explicit and !self.hoisted) return;
501
502    const gpa = macho_file.base.comp.gpa;
503
504    for (self.exports.items(.flags), self.globals.items, 0..) |flags, *global, i| {
505        const gop = try macho_file.resolver.getOrPut(gpa, .{
506            .index = @intCast(i),
507            .file = self.index,
508        }, macho_file);
509        if (!gop.found_existing) {
510            gop.ref.* = .{ .index = 0, .file = 0 };
511        }
512        global.* = gop.index;
513
514        if (gop.ref.getFile(macho_file) == null) {
515            gop.ref.* = .{ .index = @intCast(i), .file = self.index };
516            continue;
517        }
518
519        if (self.asFile().getSymbolRank(.{
520            .weak = flags.weak,
521        }) < gop.ref.getSymbol(macho_file).?.getSymbolRank(macho_file)) {
522            gop.ref.* = .{ .index = @intCast(i), .file = self.index };
523        }
524    }
525}
526
527pub fn isAlive(self: Dylib, macho_file: *MachO) bool {
528    if (!macho_file.dead_strip_dylibs) return self.explicit or self.referenced or self.needed;
529    return self.referenced or self.needed;
530}
531
532pub fn markReferenced(self: *Dylib, macho_file: *MachO) void {
533    const tracy = trace(@src());
534    defer tracy.end();
535
536    for (0..self.symbols.items.len) |i| {
537        const ref = self.getSymbolRef(@intCast(i), macho_file);
538        const file = ref.getFile(macho_file) orelse continue;
539        if (file.getIndex() != self.index) continue;
540        const global = ref.getSymbol(macho_file).?;
541        if (global.isLocal()) continue;
542        self.referenced = true;
543        break;
544    }
545}
546
547pub fn calcSymtabSize(self: *Dylib, macho_file: *MachO) void {
548    const tracy = trace(@src());
549    defer tracy.end();
550
551    for (self.symbols.items, 0..) |*sym, i| {
552        const ref = self.getSymbolRef(@intCast(i), macho_file);
553        const file = ref.getFile(macho_file) orelse continue;
554        if (file.getIndex() != self.index) continue;
555        if (sym.isLocal()) continue;
556        assert(sym.flags.import);
557        sym.flags.output_symtab = true;
558        sym.addExtra(.{ .symtab = self.output_symtab_ctx.nimports }, macho_file);
559        self.output_symtab_ctx.nimports += 1;
560        self.output_symtab_ctx.strsize += @as(u32, @intCast(sym.getName(macho_file).len + 1));
561    }
562}
563
564pub fn writeSymtab(self: Dylib, macho_file: *MachO, ctx: anytype) void {
565    const tracy = trace(@src());
566    defer tracy.end();
567
568    var n_strx = self.output_symtab_ctx.stroff;
569    for (self.symbols.items, 0..) |sym, i| {
570        const ref = self.getSymbolRef(@intCast(i), macho_file);
571        const file = ref.getFile(macho_file) orelse continue;
572        if (file.getIndex() != self.index) continue;
573        const idx = sym.getOutputSymtabIndex(macho_file) orelse continue;
574        const out_sym = &ctx.symtab.items[idx];
575        out_sym.n_strx = n_strx;
576        sym.setOutputSym(macho_file, out_sym);
577        const name = sym.getName(macho_file);
578        @memcpy(ctx.strtab.items[n_strx..][0..name.len], name);
579        n_strx += @intCast(name.len);
580        ctx.strtab.items[n_strx] = 0;
581        n_strx += 1;
582    }
583}
584
585pub inline fn getUmbrella(self: Dylib, macho_file: *MachO) *Dylib {
586    return macho_file.getFile(self.umbrella).?.dylib;
587}
588
589fn addString(self: *Dylib, allocator: Allocator, name: []const u8) !MachO.String {
590    const off = @as(u32, @intCast(self.strtab.items.len));
591    try self.strtab.ensureUnusedCapacity(allocator, name.len + 1);
592    self.strtab.appendSliceAssumeCapacity(name);
593    self.strtab.appendAssumeCapacity(0);
594    return .{ .pos = off, .len = @intCast(name.len + 1) };
595}
596
597pub fn getString(self: Dylib, string: MachO.String) [:0]const u8 {
598    assert(string.pos < self.strtab.items.len and string.pos + string.len <= self.strtab.items.len);
599    if (string.len == 0) return "";
600    return self.strtab.items[string.pos..][0 .. string.len - 1 :0];
601}
602
603pub fn asFile(self: *Dylib) File {
604    return .{ .dylib = self };
605}
606
607fn addSymbol(self: *Dylib, allocator: Allocator) !Symbol.Index {
608    try self.symbols.ensureUnusedCapacity(allocator, 1);
609    return self.addSymbolAssumeCapacity();
610}
611
612fn addSymbolAssumeCapacity(self: *Dylib) Symbol.Index {
613    const index: Symbol.Index = @intCast(self.symbols.items.len);
614    const symbol = self.symbols.addOneAssumeCapacity();
615    symbol.* = .{ .file = self.index };
616    return index;
617}
618
619pub fn getSymbolRef(self: Dylib, index: Symbol.Index, macho_file: *MachO) MachO.Ref {
620    const global_index = self.globals.items[index];
621    if (macho_file.resolver.get(global_index)) |ref| return ref;
622    return .{ .index = index, .file = self.index };
623}
624
625pub fn addSymbolExtra(self: *Dylib, allocator: Allocator, extra: Symbol.Extra) !u32 {
626    const fields = @typeInfo(Symbol.Extra).@"struct".fields;
627    try self.symbols_extra.ensureUnusedCapacity(allocator, fields.len);
628    return self.addSymbolExtraAssumeCapacity(extra);
629}
630
631fn addSymbolExtraAssumeCapacity(self: *Dylib, extra: Symbol.Extra) u32 {
632    const index = @as(u32, @intCast(self.symbols_extra.items.len));
633    const fields = @typeInfo(Symbol.Extra).@"struct".fields;
634    inline for (fields) |field| {
635        self.symbols_extra.appendAssumeCapacity(switch (field.type) {
636            u32 => @field(extra, field.name),
637            else => @compileError("bad field type"),
638        });
639    }
640    return index;
641}
642
643pub fn getSymbolExtra(self: Dylib, index: u32) Symbol.Extra {
644    const fields = @typeInfo(Symbol.Extra).@"struct".fields;
645    var i: usize = index;
646    var result: Symbol.Extra = undefined;
647    inline for (fields) |field| {
648        @field(result, field.name) = switch (field.type) {
649            u32 => self.symbols_extra.items[i],
650            else => @compileError("bad field type"),
651        };
652        i += 1;
653    }
654    return result;
655}
656
657pub fn setSymbolExtra(self: *Dylib, index: u32, extra: Symbol.Extra) void {
658    const fields = @typeInfo(Symbol.Extra).@"struct".fields;
659    inline for (fields, 0..) |field, i| {
660        self.symbols_extra.items[index + i] = switch (field.type) {
661            u32 => @field(extra, field.name),
662            else => @compileError("bad field type"),
663        };
664    }
665}
666
667pub fn fmtSymtab(self: *Dylib, macho_file: *MachO) std.fmt.Alt(Format, Format.symtab) {
668    return .{ .data = .{
669        .dylib = self,
670        .macho_file = macho_file,
671    } };
672}
673
674const Format = struct {
675    dylib: *Dylib,
676    macho_file: *MachO,
677
678    fn symtab(f: Format, w: *Writer) Writer.Error!void {
679        const dylib = f.dylib;
680        const macho_file = f.macho_file;
681        try w.writeAll("  globals\n");
682        for (dylib.symbols.items, 0..) |sym, i| {
683            const ref = dylib.getSymbolRef(@intCast(i), macho_file);
684            if (ref.getFile(macho_file) == null) {
685                // TODO any better way of handling this?
686                try w.print("    {s} : unclaimed\n", .{sym.getName(macho_file)});
687            } else {
688                try w.print("    {f}\n", .{ref.getSymbol(macho_file).?.fmt(macho_file)});
689            }
690        }
691    }
692};
693
694pub const TargetMatcher = struct {
695    allocator: Allocator,
696    cpu_arch: std.Target.Cpu.Arch,
697    platform: macho.PLATFORM,
698    target_strings: std.ArrayList([]const u8) = .empty,
699
700    pub fn init(allocator: Allocator, cpu_arch: std.Target.Cpu.Arch, platform: macho.PLATFORM) !TargetMatcher {
701        var self = TargetMatcher{
702            .allocator = allocator,
703            .cpu_arch = cpu_arch,
704            .platform = platform,
705        };
706        const apple_string = try targetToAppleString(allocator, cpu_arch, platform);
707        try self.target_strings.append(allocator, apple_string);
708
709        switch (platform) {
710            .IOSSIMULATOR, .TVOSSIMULATOR, .WATCHOSSIMULATOR, .VISIONOSSIMULATOR => {
711                // For Apple simulator targets, linking gets tricky as we need to link against the simulator
712                // hosts dylibs too.
713                const host_target = try targetToAppleString(allocator, cpu_arch, .MACOS);
714                try self.target_strings.append(allocator, host_target);
715            },
716            .MACCATALYST => {
717                // Mac Catalyst is allowed to link macOS libraries in a TBD because Apple were apparently too lazy
718                // to add the proper target strings despite doing so in other places in the format???
719                try self.target_strings.append(allocator, try targetToAppleString(allocator, cpu_arch, .MACOS));
720            },
721            .MACOS => {
722                // Turns out that around 10.13/10.14 macOS release version, Apple changed the target tags in
723                // tbd files from `macosx` to `macos`. In order to be compliant and therefore actually support
724                // linking on older platforms against `libSystem.tbd`, we add `<cpu_arch>-macosx` to target_strings.
725                const fallback_target = try std.fmt.allocPrint(allocator, "{s}-macosx", .{
726                    cpuArchToAppleString(cpu_arch),
727                });
728                try self.target_strings.append(allocator, fallback_target);
729            },
730            else => {},
731        }
732
733        return self;
734    }
735
736    pub fn deinit(self: *TargetMatcher) void {
737        for (self.target_strings.items) |t| {
738            self.allocator.free(t);
739        }
740        self.target_strings.deinit(self.allocator);
741    }
742
743    inline fn cpuArchToAppleString(cpu_arch: std.Target.Cpu.Arch) []const u8 {
744        return switch (cpu_arch) {
745            .aarch64 => "arm64",
746            .x86_64 => "x86_64",
747            else => unreachable,
748        };
749    }
750
751    pub fn targetToAppleString(allocator: Allocator, cpu_arch: std.Target.Cpu.Arch, platform: macho.PLATFORM) ![]const u8 {
752        const arch = cpuArchToAppleString(cpu_arch);
753        const plat = switch (platform) {
754            .MACOS => "macos",
755            .IOS => "ios",
756            .TVOS => "tvos",
757            .WATCHOS => "watchos",
758            .VISIONOS => "xros",
759            .IOSSIMULATOR => "ios-simulator",
760            .TVOSSIMULATOR => "tvos-simulator",
761            .WATCHOSSIMULATOR => "watchos-simulator",
762            .VISIONOSSIMULATOR => "xros-simulator",
763            .BRIDGEOS => "bridgeos",
764            .MACCATALYST => "maccatalyst",
765            .DRIVERKIT => "driverkit",
766            else => unreachable,
767        };
768        return std.fmt.allocPrint(allocator, "{s}-{s}", .{ arch, plat });
769    }
770
771    fn hasValue(stack: []const []const u8, needle: []const u8) bool {
772        for (stack) |v| {
773            if (mem.eql(u8, v, needle)) return true;
774        }
775        return false;
776    }
777
778    fn matchesArch(self: TargetMatcher, archs: []const []const u8) bool {
779        return hasValue(archs, cpuArchToAppleString(self.cpu_arch));
780    }
781
782    fn matchesTarget(self: TargetMatcher, targets: []const []const u8) bool {
783        for (self.target_strings.items) |t| {
784            if (hasValue(targets, t)) return true;
785        }
786        return false;
787    }
788
789    pub fn matchesTargetTbd(self: TargetMatcher, tbd: Tbd) !bool {
790        var arena = std.heap.ArenaAllocator.init(self.allocator);
791        defer arena.deinit();
792
793        const targets = switch (tbd) {
794            .v3 => |v3| blk: {
795                var targets = std.array_list.Managed([]const u8).init(arena.allocator());
796                for (v3.archs) |arch| {
797                    if (mem.eql(u8, v3.platform, "zippered")) {
798                        // From Xcode 10.3 → 11.3.1, macos SDK .tbd files specify platform as 'zippered'
799                        // which should map to [ '<arch>-macos', '<arch>-maccatalyst' ]
800                        try targets.append(try std.fmt.allocPrint(arena.allocator(), "{s}-macos", .{arch}));
801                        try targets.append(try std.fmt.allocPrint(arena.allocator(), "{s}-maccatalyst", .{arch}));
802                    } else {
803                        try targets.append(try std.fmt.allocPrint(arena.allocator(), "{s}-{s}", .{ arch, v3.platform }));
804                    }
805                }
806                break :blk targets.items;
807            },
808            .v4 => |v4| v4.targets,
809        };
810
811        return self.matchesTarget(targets);
812    }
813};
814
815pub const Id = struct {
816    name: []const u8,
817    timestamp: u32,
818    current_version: u32,
819    compatibility_version: u32,
820
821    pub fn default(allocator: Allocator, name: []const u8) !Id {
822        return Id{
823            .name = try allocator.dupe(u8, name),
824            .timestamp = 2,
825            .current_version = 0x10000,
826            .compatibility_version = 0x10000,
827        };
828    }
829
830    pub fn fromLoadCommand(allocator: Allocator, lc: macho.dylib_command, name: []const u8) !Id {
831        return Id{
832            .name = try allocator.dupe(u8, name),
833            .timestamp = lc.dylib.timestamp,
834            .current_version = lc.dylib.current_version,
835            .compatibility_version = lc.dylib.compatibility_version,
836        };
837    }
838
839    pub fn deinit(id: Id, allocator: Allocator) void {
840        allocator.free(id.name);
841    }
842
843    pub const ParseError = fmt.ParseIntError || fmt.BufPrintError;
844
845    pub fn parseCurrentVersion(id: *Id, version: anytype) ParseError!void {
846        id.current_version = try parseVersion(version);
847    }
848
849    pub fn parseCompatibilityVersion(id: *Id, version: anytype) ParseError!void {
850        id.compatibility_version = try parseVersion(version);
851    }
852
853    fn parseVersion(version: anytype) ParseError!u32 {
854        const string = blk: {
855            switch (version) {
856                .int => |int| {
857                    var out: u32 = 0;
858                    const major = math.cast(u16, int) orelse return error.Overflow;
859                    out += @as(u32, @intCast(major)) << 16;
860                    return out;
861                },
862                .float => |float| {
863                    var buf: [256]u8 = undefined;
864                    break :blk try fmt.bufPrint(&buf, "{d}", .{float});
865                },
866                .string => |string| {
867                    break :blk string;
868                },
869            }
870        };
871
872        var out: u32 = 0;
873        var values: [3][]const u8 = undefined;
874
875        var split = mem.splitScalar(u8, string, '.');
876        var count: u4 = 0;
877        while (split.next()) |value| {
878            if (count > 2) {
879                log.debug("malformed version field: {s}", .{string});
880                return 0x10000;
881            }
882            values[count] = value;
883            count += 1;
884        }
885
886        if (count > 2) {
887            out += try fmt.parseInt(u8, values[2], 10);
888        }
889        if (count > 1) {
890            out += @as(u32, @intCast(try fmt.parseInt(u8, values[1], 10))) << 8;
891        }
892        out += @as(u32, @intCast(try fmt.parseInt(u16, values[0], 10))) << 16;
893
894        return out;
895    }
896};
897
898const Export = struct {
899    name: MachO.String,
900    flags: Flags,
901
902    const Flags = packed struct {
903        abs: bool = false,
904        weak: bool = false,
905        tlv: bool = false,
906    };
907};
908
909const std = @import("std");
910const assert = std.debug.assert;
911const fs = std.fs;
912const fmt = std.fmt;
913const log = std.log.scoped(.link);
914const macho = std.macho;
915const math = std.math;
916const mem = std.mem;
917const Allocator = mem.Allocator;
918const Path = std.Build.Cache.Path;
919const Writer = std.Io.Writer;
920
921const Dylib = @This();
922const File = @import("file.zig").File;
923const LibStub = tapi.LibStub;
924const LoadCommandIterator = macho.LoadCommandIterator;
925const MachO = @import("../MachO.zig");
926const Symbol = @import("Symbol.zig");
927const Tbd = tapi.Tbd;
928const fat = @import("fat.zig");
929const tapi = @import("../tapi.zig");
930const trace = @import("../../tracy.zig").trace;