lib/tldr/src/properties/formats/macho.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const hypothesis = @import("hypothesis");
3 const tldr = @import("tldr");
4
5 const macho = tldr.formats.macho;
6 const model = tldr.model;
7
8 const Allocator = std.mem.Allocator;
9
10 const header_size = @sizeOf(std.macho.mach_header_64);
11 const segment_command_64_size = @sizeOf(std.macho.segment_command_64);
12 const section_64_size = @sizeOf(std.macho.section_64);
13 const symtab_command_size = @sizeOf(std.macho.symtab_command);
14 const nlist_64_size = @sizeOf(std.macho.nlist_64);
15 const relocation_info_size = @sizeOf(std.macho.relocation_info);
16
17 pub fn settings(seed: u64, max_examples: usize) hypothesis.Settings {
18 var out = hypothesis.Settings.quick()
19 .withSeed(seed)
20 .withDatabase("zig-out/hypothesis-failures/tldr");
21 out.max_examples = max_examples;
22 return out;
23 }
24
25 fn drawUsize(data: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
26 return @intCast(try data.drawInteger(
27 @intCast(min),
28 @intCast(max),
29 @intCast(shrink_towards),
30 ));
31 }
32
33 const ExpectedSection = struct {
34 segment_name: []const u8,
35 name: []const u8,
36 address: u64,
37 size: u64,
38 offset: u32,
39 alignment_shift: u32,
40 relocation_offset: u32,
41 relocation_count: u32,
42 flags: u32,
43 };
44
45 const ExpectedSymbol = struct {
46 name: []const u8,
47 section_index: u8,
48 value: u64,
49 kind: u8,
50 external: bool,
51 };
52
53 const ExpectedRelocation = struct {
54 section_index: usize,
55 address: i32,
56 symbol_number: u32,
57 pc_relative: bool,
58 length: u8,
59 external: bool,
60 kind: u8,
61 };
62
63 const GeneratedObject = struct {
64 bytes: []u8,
65 target: model.Target,
66 sections: []ExpectedSection,
67 relocations: []ExpectedRelocation,
68 symbols: []ExpectedSymbol,
69 };
70
71 fn targetForCpuType(cpu_type: std.macho.cpu_type_t) model.Target {
72 if (cpu_type == std.macho.CPU_TYPE_X86_64) return .macos_x86_64_macho;
73 return .{
74 .object_format = .macho,
75 .architecture = .aarch64,
76 .endianness = .little,
77 .pointer_width_bits = 64,
78 };
79 }
80
81 fn addString(
82 allocator: Allocator,
83 table: *std.ArrayListUnmanaged(u8),
84 value: []const u8,
85 ) !u32 {
86 const offset = std.math.cast(u32, table.items.len) orelse return error.InvalidRange;
87 try table.appendSlice(allocator, value);
88 try table.append(allocator, 0);
89 return offset;
90 }
91
92 fn generatedObject(
93 data: *hypothesis.ConjectureData,
94 allocator: Allocator,
95 ) !GeneratedObject {
96 const section_count = try drawUsize(data, 0, 6, 1);
97 const symbol_count = try drawUsize(data, 0, 8, 1);
98 const cpu_type: std.macho.cpu_type_t = if (try data.drawBoolean()) std.macho.CPU_TYPE_X86_64 else std.macho.CPU_TYPE_ARM64;
99 const target = targetForCpuType(cpu_type);
100
101 const sections = try allocator.alloc(ExpectedSection, section_count);
102 const segment_load_size = segment_command_64_size + section_count * section_64_size;
103 const load_size = segment_load_size + symtab_command_size;
104 var content_cursor = header_size + load_size;
105 var total_relocation_count: usize = 0;
106
107 for (sections, 0..) |*section, index| {
108 const size = try drawUsize(data, 0, 12, 0);
109 const relocation_count = if (size == 0) 0 else try drawUsize(data, 0, 2, 0);
110 total_relocation_count += relocation_count;
111 const data_offset = if (size == 0) 0 else content_cursor;
112 content_cursor += size;
113 const relocation_offset = if (relocation_count == 0) 0 else content_cursor;
114 content_cursor += relocation_count * relocation_info_size;
115 const alignment_shift = try drawUsize(data, 0, 5, 0);
116 section.* = .{
117 .segment_name = if (index % 2 == 0) "__TEXT" else "__DATA",
118 .name = try std.fmt.allocPrint(allocator, "__sect{d}", .{index}),
119 .address = try data.drawInteger(0, 128, 0),
120 .size = size,
121 .offset = @intCast(data_offset),
122 .alignment_shift = @intCast(alignment_shift),
123 .relocation_offset = @intCast(relocation_offset),
124 .relocation_count = @intCast(relocation_count),
125 .flags = @intCast(try drawUsize(data, 0, 0xff, 0)),
126 };
127 }
128
129 const relocations = try allocator.alloc(ExpectedRelocation, total_relocation_count);
130 var string_table: std.ArrayListUnmanaged(u8) = .empty;
131 try string_table.append(allocator, 0);
132 const symbols = try allocator.alloc(ExpectedSymbol, symbol_count);
133 const string_offsets = try allocator.alloc(u32, symbol_count);
134 for (symbols, 0..) |*symbol, index| {
135 const name = try std.fmt.allocPrint(allocator, "_pbt_macho_sym_{d}", .{index});
136 string_offsets[index] = try addString(allocator, &string_table, name);
137 const section_index = if (section_count == 0) 0 else try drawUsize(data, 0, section_count, 0);
138 const external = try data.drawBoolean();
139 const kind: u8 = if (section_index == 0) @intFromBool(external) else if (external) 0x0f else 0x0e;
140 symbol.* = .{
141 .name = name,
142 .section_index = @intCast(section_index),
143 .value = try data.drawInteger(0, 256, 0),
144 .kind = kind,
145 .external = external,
146 };
147 }
148
149 const symbol_offset = content_cursor;
150 const string_offset = symbol_offset + symbol_count * nlist_64_size;
151 const total_size = string_offset + string_table.items.len;
152 const bytes = try allocator.alloc(u8, total_size);
153 @memset(bytes, 0);
154
155 writeU32(bytes, 0, std.macho.MH_MAGIC_64);
156 writeU32(bytes, 4, @bitCast(cpu_type));
157 writeU32(bytes, 12, std.macho.MH_OBJECT);
158 writeU32(bytes, 16, 2);
159 writeU32(bytes, 20, @intCast(load_size));
160
161 const segment_offset = header_size;
162 writeU32(bytes, segment_offset, @backingInt(std.macho.LC.SEGMENT_64));
163 writeU32(bytes, segment_offset + 4, @intCast(segment_load_size));
164 writeName(bytes, segment_offset + 8, 16, "__TEXT");
165 writeU32(bytes, segment_offset + 64, @intCast(section_count));
166
167 for (sections, 0..) |section, index| {
168 const section_offset = segment_offset + segment_command_64_size + index * section_64_size;
169 writeName(bytes, section_offset, 16, section.name);
170 writeName(bytes, section_offset + 16, 16, section.segment_name);
171 writeU64(bytes, section_offset + 32, section.address);
172 writeU64(bytes, section_offset + 40, section.size);
173 writeU32(bytes, section_offset + 48, section.offset);
174 writeU32(bytes, section_offset + 52, section.alignment_shift);
175 writeU32(bytes, section_offset + 56, section.relocation_offset);
176 writeU32(bytes, section_offset + 60, section.relocation_count);
177 writeU32(bytes, section_offset + 64, section.flags);
178 if (section.size != 0) {
179 const size: usize = @intCast(section.size);
180 @memset(bytes[section.offset..][0..size], @intCast(index + 1));
181 }
182 }
183
184 var relocation_cursor: usize = 0;
185 for (sections, 0..) |section, section_index| {
186 var index: usize = 0;
187 while (index < section.relocation_count) : (index += 1) {
188 const relocation_offset = @as(usize, section.relocation_offset) + index * relocation_info_size;
189 const external = symbol_count != 0 and try data.drawBoolean();
190 const symbol_number = if (external)
191 try drawUsize(data, 0, symbol_count - 1, 0)
192 else if (section_count == 0)
193 0
194 else
195 try drawUsize(data, 1, section_count, 1);
196 const length = try drawUsize(data, 0, maxRelocationLength(section.size), 0);
197 const width = relocationWidth(@intCast(length));
198 const relocation = ExpectedRelocation{
199 .section_index = section_index,
200 .address = @intCast(try drawUsize(data, 0, try checkedUsize(section.size - width), 0)),
201 .symbol_number = @intCast(symbol_number),
202 .pc_relative = try data.drawBoolean(),
203 .length = @intCast(length),
204 .external = external,
205 .kind = @intCast(try drawUsize(data, 0, 10, 0)),
206 };
207 relocations[relocation_cursor] = relocation;
208 writeRelocation(bytes, relocation_offset, relocation);
209 relocation_cursor += 1;
210 }
211 }
212
213 const symtab_offset = segment_offset + segment_load_size;
214 writeU32(bytes, symtab_offset, @backingInt(std.macho.LC.SYMTAB));
215 writeU32(bytes, symtab_offset + 4, symtab_command_size);
216 writeU32(bytes, symtab_offset + 8, @intCast(symbol_offset));
217 writeU32(bytes, symtab_offset + 12, @intCast(symbol_count));
218 writeU32(bytes, symtab_offset + 16, @intCast(string_offset));
219 writeU32(bytes, symtab_offset + 20, @intCast(string_table.items.len));
220
221 for (symbols, 0..) |symbol, index| {
222 const offset = symbol_offset + index * nlist_64_size;
223 writeU32(bytes, offset, string_offsets[index]);
224 bytes[offset + 4] = symbol.kind;
225 bytes[offset + 5] = symbol.section_index;
226 writeU64(bytes, offset + 8, symbol.value);
227 }
228 @memcpy(bytes[string_offset..][0..string_table.items.len], string_table.items);
229
230 return .{
231 .bytes = bytes,
232 .target = target,
233 .sections = sections,
234 .relocations = relocations,
235 .symbols = symbols,
236 };
237 }
238
239 pub const MetadataProperty = struct {
240 pub fn property(data: *hypothesis.ConjectureData, property_allocator: Allocator) !void {
241 var arena_state = std.heap.ArenaAllocator.init(property_allocator);
242 defer arena_state.deinit();
243 const allocator = arena_state.allocator();
244
245 const generated = try generatedObject(data, allocator);
246 var object = try macho.parseObject(allocator, generated.bytes);
247 defer object.deinit(allocator);
248
249 try std.testing.expectEqual(generated.target.object_format, object.target.object_format);
250 try std.testing.expectEqual(generated.target.architecture, object.target.architecture);
251 try std.testing.expectEqual(generated.sections.len, object.sections.len);
252 for (generated.sections, object.sections) |expected, actual| {
253 try std.testing.expectEqualStrings(expected.segment_name, actual.segment_name);
254 try std.testing.expectEqualStrings(expected.name, actual.name);
255 try std.testing.expectEqual(expected.address, actual.address);
256 try std.testing.expectEqual(expected.size, actual.size);
257 try std.testing.expectEqual(expected.offset, actual.offset);
258 try std.testing.expectEqual(expected.alignment_shift, actual.alignment_shift);
259 try std.testing.expectEqual(expected.relocation_offset, actual.relocation_offset);
260 try std.testing.expectEqual(expected.relocation_count, actual.relocation_count);
261 try std.testing.expectEqual(expected.flags, actual.flags);
262 }
263 try std.testing.expectEqual(generated.relocations.len, object.relocations.len);
264 for (generated.relocations, object.relocations) |expected, actual| {
265 try std.testing.expectEqual(expected.section_index, actual.section_index);
266 try std.testing.expectEqual(expected.address, actual.address);
267 try std.testing.expectEqual(expected.symbol_number, actual.symbol_number);
268 try std.testing.expectEqual(expected.pc_relative, actual.pc_relative);
269 try std.testing.expectEqual(expected.length, actual.length);
270 try std.testing.expectEqual(expected.external, actual.external);
271 try std.testing.expectEqual(expected.kind, actual.kind);
272 }
273 try std.testing.expectEqual(generated.symbols.len, object.symbols.len);
274 for (generated.symbols, object.symbols) |expected, actual| {
275 try std.testing.expectEqualStrings(expected.name, actual.name);
276 try std.testing.expectEqual(expected.section_index, actual.section_index);
277 try std.testing.expectEqual(expected.value, actual.value);
278 try std.testing.expectEqual(expected.kind, actual.kind);
279 try std.testing.expectEqual(expected.external, actual.external);
280 }
281
282 var metadata = try macho.parseObjectMetadata(allocator, generated.bytes);
283 defer metadata.deinit(allocator);
284
285 try std.testing.expectEqual(generated.sections.len, metadata.sections.len);
286 try std.testing.expectEqual(generated.symbols.len, metadata.symbols.len);
287 for (generated.sections, metadata.sections) |expected, actual| {
288 try std.testing.expectEqualStrings(expected.segment_name, actual.segment_name);
289 try std.testing.expectEqualStrings(expected.name, actual.name);
290 try std.testing.expectEqual(expected.size, actual.size);
291 try std.testing.expectEqual(try alignmentFromShift(expected.alignment_shift), actual.alignment);
292 try std.testing.expectEqual(expected.relocation_count, actual.relocation_count);
293 }
294 for (generated.symbols, metadata.symbols) |expected, actual| {
295 try std.testing.expectEqualStrings(expected.name, actual.name);
296 try std.testing.expectEqual(@as(i32, expected.section_index), actual.section_index);
297 try std.testing.expectEqual(expected.external, actual.external);
298 try std.testing.expectEqual(expected.section_index == 0, actual.undefined);
299 }
300 }
301 };
302
303 test "pbt: Mach-O parser preserves generated object metadata" {
304 try hypothesis.checkNamed(
305 MetadataProperty,
306 "tldr-macho-object-metadata",
307 settings(0xBADC_0FFE, 48),
308 );
309 }
310
311 fn alignmentFromShift(shift: u32) !u64 {
312 if (shift >= 63) return error.InvalidAlignment;
313 return @as(u64, 1) << @intCast(shift);
314 }
315
316 fn relocationWidth(length: u8) u64 {
317 return @as(u64, 1) << @intCast(length);
318 }
319
320 fn maxRelocationLength(section_size: u64) usize {
321 if (section_size >= 8) return 3;
322 if (section_size >= 4) return 2;
323 if (section_size >= 2) return 1;
324 return 0;
325 }
326
327 fn checkedUsize(value: anytype) !usize {
328 return std.math.cast(usize, value) orelse error.InvalidRange;
329 }
330
331 fn writeU32(bytes: []u8, offset: usize, value: u32) void {
332 std.mem.writeInt(u32, bytes[offset..][0..4], value, .little);
333 }
334
335 fn writeI32(bytes: []u8, offset: usize, value: i32) void {
336 std.mem.writeInt(i32, bytes[offset..][0..4], value, .little);
337 }
338
339 fn writeU64(bytes: []u8, offset: usize, value: u64) void {
340 std.mem.writeInt(u64, bytes[offset..][0..8], value, .little);
341 }
342
343 fn writeName(bytes: []u8, offset: usize, comptime size: usize, value: []const u8) void {
344 @memset(bytes[offset..][0..size], 0);
345 @memcpy(bytes[offset..][0..value.len], value);
346 }
347
348 fn writeRelocation(bytes: []u8, offset: usize, relocation: ExpectedRelocation) void {
349 writeI32(bytes, offset, relocation.address);
350 const encoded = (relocation.symbol_number & 0x00ff_ffff) |
351 (@as(u32, @intFromBool(relocation.pc_relative)) << 24) |
352 (@as(u32, relocation.length) << 25) |
353 (@as(u32, @intFromBool(relocation.external)) << 27) |
354 (@as(u32, relocation.kind) << 28);
355 writeU32(bytes, offset + 4, encoded);
356 }