lib/tldr/src/incremental/format.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const builtin = @import("builtin");
  3 const incremental = @import("root.zig");
  4 const parallel = @import("../root.zig").parallel;
  5 
  6 const Allocator = std.mem.Allocator;
  7 const ContributionKind = incremental.ContributionKind;
  8 const ContributionRecord = incremental.ContributionRecord;
  9 const DiscardReason = incremental.DiscardReason;
 10 const DiscardedContributionRecord = incremental.DiscardedContributionRecord;
 11 const ArchiveMemberRecord = incremental.ArchiveMemberRecord;
 12 const GotEntryRecord = incremental.GotEntryRecord;
 13 const MergePieceRecord = incremental.MergePieceRecord;
 14 const ExternalTargetRecord = incremental.ExternalTargetRecord;
 15 const InputRecord = incremental.InputRecord;
 16 const Manifest = incremental.Manifest;
 17 const SectionRecord = incremental.SectionRecord;
 18 const StringSpan = incremental.StringSpan;
 19 const Strings = incremental.Strings;
 20 
 21 const manifest_binary_magic = [_]u8{ 'T', 'L', 'D', 'R', 'I', 'M', 'F', 10 };
 22 
 23 const file_endianness: u8 = switch (builtin.cpu.arch.endian()) {
 24     .little => 0,
 25     .big => 1,
 26 };
 27 
 28 const LinkMapTotals = struct {
 29     retained_size: u64 = 0,
 30     retained_reserve: u64 = 0,
 31     discarded_size: u64 = 0,
 32 };
 33 
 34 const BinaryReader = struct {
 35     bytes: []const u8,
 36     index: usize = 0,
 37 
 38     fn take(self: *BinaryReader, len: usize) ![]const u8 {
 39         if (self.index > self.bytes.len or len > self.bytes.len - self.index) return error.InvalidManifestBinary;
 40         const result = self.bytes[self.index..][0..len];
 41         self.index += len;
 42         return result;
 43     }
 44 
 45     fn readU8(self: *BinaryReader) !u8 {
 46         return (try self.take(1))[0];
 47     }
 48 
 49     fn readU16(self: *BinaryReader) !u16 {
 50         return std.mem.readInt(u16, (try self.take(2))[0..2], .little);
 51     }
 52 
 53     fn readU32(self: *BinaryReader) !u32 {
 54         return std.mem.readInt(u32, (try self.take(4))[0..4], .little);
 55     }
 56 
 57     fn readU64(self: *BinaryReader) !u64 {
 58         return std.mem.readInt(u64, (try self.take(8))[0..8], .little);
 59     }
 60 
 61     fn readBool(self: *BinaryReader) !bool {
 62         return switch (try self.readU8()) {
 63             0 => false,
 64             1 => true,
 65             else => error.InvalidManifestBinary,
 66         };
 67     }
 68 
 69     fn readEnum(self: *BinaryReader, comptime Enum: type) !Enum {
 70         const value = try self.readU8();
 71         inline for (
 72             @typeInfo(Enum).@"enum".field_names,
 73             @typeInfo(Enum).@"enum".field_values,
 74         ) |field_name, field_name_value| {
 75             const field = .{ .name = field_name, .value = field_name_value };
 76             if (field.value == value) return @fromBackingInt(@intCast(field.value));
 77         }
 78         return error.InvalidManifestBinary;
 79     }
 80 
 81     fn finish(self: BinaryReader) !void {
 82         if (self.index != self.bytes.len) return error.InvalidManifestBinary;
 83     }
 84 };
 85 
 86 const LinkMapContributionSummary = struct {
 87     count: usize = 0,
 88     size: u64 = 0,
 89     reserved_size: u64 = 0,
 90 };
 91 
 92 const LinkMapDiscardSummary = struct {
 93     count: usize = 0,
 94     size: u64 = 0,
 95 };
 96 
 97 const link_map_discard_reasons = [_]DiscardReason{
 98     .discarded,
 99     .identical_code_folded,
100 };
101 
102 pub fn textAlloc(manifest: Manifest, allocator: Allocator) ![]u8 {
103     var out = std.Io.Writer.Allocating.init(allocator);
104     defer out.deinit();
105     try writeText(manifest, &out.writer);
106     return try out.toOwnedSlice();
107 }
108 
109 pub fn binaryAlloc(manifest: Manifest, allocator: Allocator) ![]u8 {
110     var out = std.Io.Writer.Allocating.init(allocator);
111     defer out.deinit();
112     try writeBinary(manifest, &out.writer);
113     return try out.toOwnedSlice();
114 }
115 
116 pub fn manifestFromBinary(allocator: Allocator, bytes: []u8) !Manifest {
117     const walked = try walkBinary(bytes);
118 
119     const manifest: Manifest = .{
120         .target = .{
121             .object_format = try enumFromByte(@TypeOf(Manifest.empty().target.object_format), walked.header.object_format),
122             .architecture = try enumFromByte(@TypeOf(Manifest.empty().target.architecture), walked.header.architecture),
123             .endianness = try enumFromByte(@TypeOf(Manifest.empty().target.endianness), walked.header.endianness),
124             .pointer_width_bits = walked.header.pointer_width_bits,
125         },
126         .output_kind = try enumFromByte(@TypeOf(Manifest.empty().output_kind), walked.header.output_kind),
127         .entry_symbol_id = walked.header.entry_symbol_id,
128         .image_base = walked.header.image_base,
129         .page_size = walked.header.page_size,
130         .gc_sections = try boolFromByte(walked.header.gc_sections),
131         .icf = try enumFromByte(@TypeOf(Manifest.empty().icf), walked.header.icf),
132         .strip_debug = try boolFromByte(walked.header.strip_debug),
133         .build_id = try enumFromByte(@TypeOf(Manifest.empty().build_id), walked.header.build_id),
134         .input_hashes_recorded = try boolFromByte(walked.header.input_hashes_recorded),
135         .inputs_read_at_ns = @bitCast(walked.header.inputs_read_at_ns),
136         .strings = .{
137             .spans = walked.spans,
138             .blob = walked.blob,
139         },
140         .inputs = recordSlice(InputRecord, bytes, walked.sections[0]),
141         .sections = recordSlice(SectionRecord, bytes, walked.sections[1]),
142         .contributions = recordSlice(ContributionRecord, bytes, walked.sections[2]),
143         .discarded_contributions = recordSlice(DiscardedContributionRecord, bytes, walked.sections[3]),
144         .external_targets = recordSlice(ExternalTargetRecord, bytes, walked.sections[4]),
145         .archive_members = recordSlice(ArchiveMemberRecord, bytes, walked.sections[5]),
146         .got_entries = recordSlice(GotEntryRecord, bytes, walked.sections[6]),
147         .merge_pieces = recordSlice(MergePieceRecord, bytes, walked.sections[7]),
148     };
149 
150     const string_count = std.math.cast(u32, walked.spans.len) orelse return error.InvalidManifestBinary;
151     if (string_count != 0 and manifest.entry_symbol_id >= string_count) return error.InvalidManifestBinary;
152     for (walked.spans) |span| {
153         if (span.offset > walked.blob.len or span.len > walked.blob.len - span.offset) {
154             return error.InvalidManifestBinary;
155         }
156     }
157 
158     try validateRecords(allocator, bytes, walked.sections, string_count);
159     return manifest;
160 }
161 
162 const binary_header_scalar_bytes = 1 + 1 + 1 + 2 + 1 + 4 + 8 + 8 + 1 + 1 + 1 + 1 + 1 + 8;
163 const header_read_at_patch_offset = binary_header_scalar_bytes - 8;
164 
165 const BinaryHeader = struct {
166     object_format: u8,
167     architecture: u8,
168     endianness: u8,
169     pointer_width_bits: u16,
170     output_kind: u8,
171     entry_symbol_id: u32,
172     image_base: u64,
173     page_size: u64,
174     gc_sections: u8,
175     icf: u8,
176     strip_debug: u8,
177     build_id: u8,
178     input_hashes_recorded: u8,
179     inputs_read_at_ns: u64,
180 };
181 
182 const WalkedBinary = struct {
183     header_offset: usize,
184     header: BinaryHeader,
185     spans: []const StringSpan,
186     blob: []const u8,
187     sections: [record_section_count]RecordSectionLayout,
188 };
189 
190 fn walkBinary(bytes: []const u8) !WalkedBinary {
191     var reader: BinaryReader = .{ .bytes = bytes };
192     const magic = try reader.take(manifest_binary_magic.len);
193     if (!std.mem.eql(u8, magic, &manifest_binary_magic)) return error.UnsupportedManifestProtocol;
194     const protocol_len = std.math.cast(usize, try reader.readU32()) orelse return error.InvalidManifestBinary;
195     const protocol = try reader.take(protocol_len);
196     if (!std.mem.eql(u8, protocol, incremental.manifest_binary_protocol)) return error.UnsupportedManifestProtocol;
197     if (try reader.readU8() != file_endianness) return error.UnsupportedManifestProtocol;
198 
199     const header_offset = reader.index;
200     const header = BinaryHeader{
201         .object_format = try reader.readU8(),
202         .architecture = try reader.readU8(),
203         .endianness = try reader.readU8(),
204         .pointer_width_bits = try reader.readU16(),
205         .output_kind = try reader.readU8(),
206         .entry_symbol_id = try reader.readU32(),
207         .image_base = try reader.readU64(),
208         .page_size = try reader.readU64(),
209         .gc_sections = try reader.readU8(),
210         .icf = try reader.readU8(),
211         .strip_debug = try reader.readU8(),
212         .build_id = try reader.readU8(),
213         .input_hashes_recorded = try reader.readU8(),
214         .inputs_read_at_ns = try reader.readU64(),
215     };
216 
217     const string_count = std.math.cast(usize, try reader.readU32()) orelse return error.InvalidManifestBinary;
218     const spans_bytes = try reader.take(string_count * @sizeOf(StringSpan));
219     const spans = std.mem.bytesAsSlice(StringSpan, spans_bytes);
220     const blob_len = std.math.cast(usize, try reader.readU32()) orelse return error.InvalidManifestBinary;
221     const blob = try reader.take(blob_len);
222 
223     var sections: [record_section_count]RecordSectionLayout = undefined;
224     for (&sections, 0..) |*section, section_index| {
225         const count = std.math.cast(usize, try reader.readU32()) orelse return error.InvalidManifestBinary;
226         const span = std.math.mul(usize, count, record_binary_sizes[section_index]) catch return error.InvalidManifestBinary;
227         section.* = .{ .count = count, .offset = reader.index };
228         _ = try reader.take(span);
229     }
230     try reader.finish();
231 
232     return .{
233         .header_offset = header_offset,
234         .header = header,
235         .spans = spans,
236         .blob = blob,
237         .sections = sections,
238     };
239 }
240 
241 fn recordSlice(comptime Record: type, bytes: []u8, layout: RecordSectionLayout) []Record {
242     if (layout.count == 0) return &.{};
243     return std.mem.bytesAsSlice(Record, bytes[layout.offset..][0 .. layout.count * @sizeOf(Record)]);
244 }
245 
246 fn enumFromByte(comptime Enum: type, byte: u8) !Enum {
247     inline for (std.meta.tags(Enum)) |value| {
248         if (@backingInt(value) == byte) return value;
249     }
250     return error.InvalidManifestBinary;
251 }
252 
253 fn boolFromByte(byte: u8) !bool {
254     return switch (byte) {
255         0 => false,
256         1 => true,
257         else => error.InvalidManifestBinary,
258     };
259 }
260 
261 const record_section_count = 8;
262 
263 pub const record_binary_sizes = [record_section_count]usize{
264     @sizeOf(InputRecord),
265     @sizeOf(SectionRecord),
266     @sizeOf(ContributionRecord),
267     @sizeOf(DiscardedContributionRecord),
268     @sizeOf(ExternalTargetRecord),
269     @sizeOf(ArchiveMemberRecord),
270     @sizeOf(GotEntryRecord),
271     @sizeOf(MergePieceRecord),
272 };
273 
274 const record_chunk_len = 65536;
275 
276 const RecordSectionLayout = struct {
277     count: usize,
278     offset: usize,
279 };
280 
281 const ValidateTask = struct {
282     section: usize,
283     start: usize,
284     len: usize,
285 };
286 
287 const ValidateFailure = struct {
288     section: usize = std.math.maxInt(usize),
289     start: usize = 0,
290     err: ?anyerror = null,
291 
292     fn found(self: ValidateFailure) bool {
293         return self.err != null;
294     }
295 
296     fn before(self: ValidateFailure, other: ValidateFailure) bool {
297         if (self.section != other.section) return self.section < other.section;
298         return self.start < other.start;
299     }
300 };
301 
302 const ValidateFailures = parallel.FailureSlots(ValidateFailure);
303 
304 const ValidateContext = struct {
305     bytes: []const u8,
306     sections: [record_section_count]RecordSectionLayout,
307     string_count: u32,
308     tasks: []const ValidateTask = &.{},
309     failures: ?*ValidateFailures = null,
310 };
311 
312 fn validateRecords(
313     allocator: Allocator,
314     bytes: []const u8,
315     sections: [record_section_count]RecordSectionLayout,
316     string_count: u32,
317 ) !void {
318     var context = ValidateContext{
319         .bytes = bytes,
320         .sections = sections,
321         .string_count = string_count,
322     };
323 
324     var total_records: usize = 0;
325     for (sections) |section| total_records += section.count;
326 
327     if (total_records < record_chunk_len or !parallel.overlapAvailable()) {
328         for (sections, 0..) |section, section_index| {
329             try validateRecordRange(&context, section_index, 0, section.count);
330         }
331         return;
332     }
333 
334     var tasks = std.ArrayListUnmanaged(ValidateTask).empty;
335     defer tasks.deinit(allocator);
336     for (sections, 0..) |section, section_index| {
337         var start: usize = 0;
338         while (start < section.count) {
339             const len = @min(record_chunk_len, section.count - start);
340             try tasks.append(allocator, .{ .section = section_index, .start = start, .len = len });
341             start += len;
342         }
343     }
344     if (tasks.items.len == 0) return;
345 
346     const workers = parallel.chooseWorkers(tasks.items.len, 0);
347     var failures = try ValidateFailures.init(allocator, workers, .{});
348     defer failures.deinit(allocator);
349     context.tasks = tasks.items;
350     context.failures = &failures;
351     parallel.forItems(tasks.items.len, workers, &context, validateTaskItem);
352     if (failures.earliest(ValidateFailure.found, ValidateFailure.before)) |failure| {
353         return failure.err.?;
354     }
355 }
356 
357 fn validateTaskItem(context: *ValidateContext, worker: usize, task_index: usize) void {
358     const task = context.tasks[task_index];
359     validateRecordRange(context, task.section, task.start, task.len) catch |err| {
360         const failures = context.failures.?;
361         const current = failures.items[worker];
362         const failure = ValidateFailure{ .section = task.section, .start = task.start, .err = err };
363         if (!current.found() or ValidateFailure.before(failure, current)) {
364             failures.record(worker, failure);
365         }
366     };
367 }
368 
369 const FieldCheck = struct {
370     offset: usize,
371     kind: enum { string_id, boolean, contribution_kind, discard_reason },
372 };
373 
374 const section_field_checks = [record_section_count][]const FieldCheck{
375     &.{
376         .{ .offset = @offsetOf(InputRecord, "name_id"), .kind = .string_id },
377         .{ .offset = @offsetOf(InputRecord, "identity_recorded"), .kind = .boolean },
378     },
379     &.{
380         .{ .offset = @offsetOf(SectionRecord, "name_id"), .kind = .string_id },
381     },
382     &.{
383         .{ .offset = @offsetOf(ContributionRecord, "input_name_id"), .kind = .string_id },
384         .{ .offset = @offsetOf(ContributionRecord, "name_id"), .kind = .string_id },
385         .{ .offset = @offsetOf(ContributionRecord, "output_section_name_id"), .kind = .string_id },
386         .{ .offset = @offsetOf(ContributionRecord, "kind"), .kind = .contribution_kind },
387     },
388     &.{
389         .{ .offset = @offsetOf(DiscardedContributionRecord, "input_name_id"), .kind = .string_id },
390         .{ .offset = @offsetOf(DiscardedContributionRecord, "name_id"), .kind = .string_id },
391         .{ .offset = @offsetOf(DiscardedContributionRecord, "reason"), .kind = .discard_reason },
392     },
393     &.{
394         .{ .offset = @offsetOf(ExternalTargetRecord, "name_id"), .kind = .string_id },
395         .{ .offset = @offsetOf(ExternalTargetRecord, "address_signed"), .kind = .boolean },
396     },
397     &.{
398         .{ .offset = @offsetOf(ArchiveMemberRecord, "name_id"), .kind = .string_id },
399         .{ .offset = @offsetOf(ArchiveMemberRecord, "selected"), .kind = .boolean },
400     },
401     &.{
402         .{ .offset = @offsetOf(GotEntryRecord, "input_name_id"), .kind = .string_id },
403         .{ .offset = @offsetOf(GotEntryRecord, "name_id"), .kind = .string_id },
404     },
405     &.{
406         .{ .offset = @offsetOf(MergePieceRecord, "input_name_id"), .kind = .string_id },
407     },
408 };
409 
410 fn validateRecordRange(context: *ValidateContext, section_index: usize, start: usize, len: usize) !void {
411     const layout = context.sections[section_index];
412     const record_size = record_binary_sizes[section_index];
413     const checks = section_field_checks[section_index];
414     var index = start;
415     while (index < start + len) : (index += 1) {
416         const record_bytes = context.bytes[layout.offset + index * record_size ..][0..record_size];
417         for (checks) |check| {
418             switch (check.kind) {
419                 .string_id => {
420                     const id = std.mem.readInt(u32, record_bytes[check.offset..][0..4], .little);
421                     if (id >= context.string_count) return error.InvalidManifestBinary;
422                 },
423                 .boolean => _ = try boolFromByte(record_bytes[check.offset]),
424                 .contribution_kind => _ = try enumFromByte(ContributionKind, record_bytes[check.offset]),
425                 .discard_reason => _ = try enumFromByte(DiscardReason, record_bytes[check.offset]),
426             }
427         }
428     }
429 }
430 
431 pub const ManifestBinaryLayout = struct {
432     header_offset: usize,
433     sections: [record_section_count]RecordSectionLayout,
434 };
435 
436 pub fn recordSectionLayouts(bytes: []const u8) !ManifestBinaryLayout {
437     const walked = try walkBinary(bytes);
438     return .{ .header_offset = walked.header_offset, .sections = walked.sections };
439 }
440 
441 pub const manifest_field_patch_capacity = 40;
442 
443 pub const ManifestFieldPatch = struct {
444     offset: usize,
445     len: usize,
446     bytes: [manifest_field_patch_capacity]u8,
447 
448     pub fn slice(self: *const ManifestFieldPatch) []const u8 {
449         return self.bytes[0..self.len];
450     }
451 };
452 
453 const input_scalar_patch_offset = @offsetOf(InputRecord, "size");
454 const input_scalar_patch_len = @offsetOf(InputRecord, "identity_recorded") + 1 - input_scalar_patch_offset;
455 const contribution_scalar_patch_offset = @offsetOf(ContributionRecord, "size");
456 const contribution_scalar_patch_len = @offsetOf(ContributionRecord, "file_size") + 8 - contribution_scalar_patch_offset;
457 const archive_member_scalar_patch_offset = @offsetOf(ArchiveMemberRecord, "hash");
458 const archive_member_scalar_patch_len = @offsetOf(ArchiveMemberRecord, "link_hash") + 8 - archive_member_scalar_patch_offset;
459 
460 pub fn manifestScalarPatchesAlloc(
461     allocator: Allocator,
462     encoded: []const u8,
463     manifest: Manifest,
464     input_indexes: []const usize,
465     contribution_indexes: []const usize,
466     archive_member_indexes: []const usize,
467 ) ![]ManifestFieldPatch {
468     const layout = try recordSectionLayouts(encoded);
469     const sections = layout.sections;
470     const patches = try allocator.alloc(
471         ManifestFieldPatch,
472         1 + input_indexes.len + contribution_indexes.len + archive_member_indexes.len,
473     );
474     errdefer allocator.free(patches);
475     var cursor: usize = 0;
476     var read_at_patch = ManifestFieldPatch{
477         .offset = layout.header_offset + header_read_at_patch_offset,
478         .len = 8,
479         .bytes = undefined,
480     };
481     std.mem.writeInt(u64, read_at_patch.bytes[0..8], @bitCast(manifest.inputs_read_at_ns), .little);
482     patches[cursor] = read_at_patch;
483     cursor += 1;
484     for (input_indexes) |index| {
485         if (index >= sections[0].count or index >= manifest.inputs.len) return error.InvalidManifestBinary;
486         patches[cursor] = recordBytesPatch(
487             InputRecord,
488             &manifest.inputs[index],
489             sections[0].offset + index * record_binary_sizes[0],
490             input_scalar_patch_offset,
491             input_scalar_patch_len,
492         );
493         cursor += 1;
494     }
495     for (contribution_indexes) |index| {
496         if (index >= sections[2].count or index >= manifest.contributions.len) return error.InvalidManifestBinary;
497         patches[cursor] = recordBytesPatch(
498             ContributionRecord,
499             &manifest.contributions[index],
500             sections[2].offset + index * record_binary_sizes[2],
501             contribution_scalar_patch_offset,
502             contribution_scalar_patch_len,
503         );
504         cursor += 1;
505     }
506     for (archive_member_indexes) |index| {
507         if (index >= sections[5].count or index >= manifest.archive_members.len) return error.InvalidManifestBinary;
508         patches[cursor] = recordBytesPatch(
509             ArchiveMemberRecord,
510             &manifest.archive_members[index],
511             sections[5].offset + index * record_binary_sizes[5],
512             archive_member_scalar_patch_offset,
513             archive_member_scalar_patch_len,
514         );
515         cursor += 1;
516     }
517     return patches;
518 }
519 
520 fn recordBytesPatch(
521     comptime Record: type,
522     record: *const Record,
523     record_offset: usize,
524     field_offset: usize,
525     len: usize,
526 ) ManifestFieldPatch {
527     var patch = ManifestFieldPatch{
528         .offset = record_offset + field_offset,
529         .len = len,
530         .bytes = undefined,
531     };
532     const source = std.mem.asBytes(record)[field_offset..][0..len];
533     @memcpy(patch.bytes[0..len], source);
534     return patch;
535 }
536 
537 pub fn writeBinary(manifest: Manifest, writer: *std.Io.Writer) !void {
538     try writer.writeAll(&manifest_binary_magic);
539     try writeStringBinary(writer, incremental.manifest_binary_protocol);
540     try writer.writeByte(file_endianness);
541 
542     try writeEnumBinary(writer, manifest.target.object_format);
543     try writeEnumBinary(writer, manifest.target.architecture);
544     try writeEnumBinary(writer, manifest.target.endianness);
545     try writeU16Binary(writer, manifest.target.pointer_width_bits);
546     try writeEnumBinary(writer, manifest.output_kind);
547     try writeU32Binary(writer, manifest.entry_symbol_id);
548     try writeU64Binary(writer, manifest.image_base);
549     try writeU64Binary(writer, manifest.page_size);
550     try writeBoolBinary(writer, manifest.gc_sections);
551     try writeEnumBinary(writer, manifest.icf);
552     try writeBoolBinary(writer, manifest.strip_debug);
553     try writeEnumBinary(writer, manifest.build_id);
554     try writeBoolBinary(writer, manifest.input_hashes_recorded);
555     try writeU64Binary(writer, @bitCast(manifest.inputs_read_at_ns));
556 
557     const strings = &manifest.strings;
558     try writeCountBinary(writer, strings.count());
559     const tier0_blob_len = std.math.cast(u32, strings.blob.len) orelse return error.ManifestTooLarge;
560     for (strings.spans) |span| {
561         try writeU32Binary(writer, span.offset);
562         try writeU32Binary(writer, span.len);
563     }
564     for (strings.appendix_spans.items) |span| {
565         const shifted = std.math.add(u32, span.offset, tier0_blob_len) catch return error.ManifestTooLarge;
566         try writeU32Binary(writer, shifted);
567         try writeU32Binary(writer, span.len);
568     }
569     const total_blob = std.math.add(usize, strings.blob.len, strings.appendix.items.len) catch return error.ManifestTooLarge;
570     try writeCountBinary(writer, total_blob);
571     try writer.writeAll(strings.blob);
572     try writer.writeAll(strings.appendix.items);
573 
574     try writeRecordSection(writer, manifest.inputs);
575     try writeRecordSection(writer, manifest.sections);
576     try writeRecordSection(writer, manifest.contributions);
577     try writeRecordSection(writer, manifest.discarded_contributions);
578     try writeRecordSection(writer, manifest.external_targets);
579     try writeRecordSection(writer, manifest.archive_members);
580     try writeRecordSection(writer, manifest.got_entries);
581     try writeRecordSection(writer, manifest.merge_pieces);
582 }
583 
584 fn writeRecordSection(writer: *std.Io.Writer, records: anytype) !void {
585     try writeCountBinary(writer, records.len);
586     if (records.len != 0) {
587         try writer.writeAll(std.mem.sliceAsBytes(records));
588     }
589 }
590 
591 pub fn writeText(manifest: Manifest, writer: *std.Io.Writer) std.Io.Writer.Error!void {
592     const totals = linkMapTotals(manifest);
593 
594     try writer.writeAll("tldr link map\n");
595     try writer.print(
596         "target: {s}/{s} {s} ptr={d}\n",
597         .{
598             @tagName(manifest.target.object_format),
599             @tagName(manifest.target.architecture),
600             @tagName(manifest.target.endianness),
601             manifest.target.pointer_width_bits,
602         },
603     );
604     try writer.print(
605         "output: {s} entry={s} image_base=0x{x} page_size=0x{x}\n",
606         .{ @tagName(manifest.output_kind), manifest.entrySymbol(), manifest.image_base, manifest.page_size },
607     );
608     try writer.print("options: gc_sections={} icf={s} strip_debug={} build_id={s}\n", .{ manifest.gc_sections, @tagName(manifest.icf), manifest.strip_debug, @tagName(manifest.build_id) });
609     try writer.print(
610         "summary: inputs={d} sections={d} retained={d} retained_size={d} retained_reserve={d} discarded={d} discarded_size={d} external_targets={d} archive_members={d} got_entries={d} merge_pieces={d}\n",
611         .{
612             manifest.inputs.len,
613             manifest.sections.len,
614             manifest.contributions.len,
615             totals.retained_size,
616             totals.retained_reserve,
617             manifest.discarded_contributions.len,
618             totals.discarded_size,
619             manifest.external_targets.len,
620             manifest.archive_members.len,
621             manifest.got_entries.len,
622             manifest.merge_pieces.len,
623         },
624     );
625     if (manifest.contributions.len != 0) {
626         try writer.writeAll("retained by output:\n");
627         for (manifest.sections) |section| {
628             const summary = outputContributionSummary(manifest, section.name_id);
629             if (summary.count == 0) continue;
630             try writer.print(
631                 "  {s} count={d} size={d} reserve={d}\n",
632                 .{ manifest.string(section.name_id), summary.count, summary.size, summary.reserved_size },
633             );
634         }
635     }
636     if (manifest.discarded_contributions.len != 0) {
637         try writer.writeAll("discarded by reason:\n");
638         for (link_map_discard_reasons) |reason| {
639             const summary = discardedReasonSummary(manifest, reason);
640             if (summary.count == 0) continue;
641             try writer.print(
642                 "  {s} count={d} size={d}\n",
643                 .{ @tagName(reason), summary.count, summary.size },
644             );
645         }
646     }
647 
648     try writer.print("inputs ({d}):\n", .{manifest.inputs.len});
649     for (manifest.inputs) |input| {
650         if (manifest.input_hashes_recorded) {
651             try writer.print("  {s} size={d} hash=0x{x}\n", .{ manifest.string(input.name_id), input.size, input.hash });
652         } else {
653             try writer.print("  {s} size={d} hash=unrecorded\n", .{ manifest.string(input.name_id), input.size });
654         }
655     }
656 
657     try writer.print("sections ({d}):\n", .{manifest.sections.len});
658     for (manifest.sections) |section| {
659         try writer.print(
660             "  {s} addr=0x{x} file=0x{x} size={d} reserve={d} align={d}\n",
661             .{
662                 manifest.string(section.name_id),
663                 section.address,
664                 section.file_offset,
665                 section.size,
666                 section.reserved_size,
667                 section.alignment,
668             },
669         );
670     }
671 
672     try writer.print("contributions ({d}):\n", .{manifest.contributions.len});
673     for (manifest.contributions) |contribution| {
674         try writer.print(
675             "  {s} <- {s}:{s}#{d} kind={s} addr=0x{x} file=0x{x} size={d} file_size={d} reserve={d} align={d}\n",
676             .{
677                 manifest.string(contribution.output_section_name_id),
678                 manifest.string(contribution.input_name_id),
679                 manifest.string(contribution.name_id),
680                 contribution.ordinal,
681                 @tagName(contribution.kind),
682                 contribution.address,
683                 contribution.file_offset,
684                 contribution.size,
685                 contribution.file_size,
686                 contribution.reserved_size,
687                 contribution.alignment,
688             },
689         );
690     }
691 
692     try writer.print("discarded ({d}):\n", .{manifest.discarded_contributions.len});
693     for (manifest.discarded_contributions) |contribution| {
694         try writer.print(
695             "  {s}:{s}#{d} reason={s} size={d} align={d}\n",
696             .{
697                 manifest.string(contribution.input_name_id),
698                 manifest.string(contribution.name_id),
699                 contribution.ordinal,
700                 @tagName(contribution.reason),
701                 contribution.size,
702                 contribution.alignment,
703             },
704         );
705     }
706 }
707 
708 fn linkMapTotals(manifest: Manifest) LinkMapTotals {
709     var totals: LinkMapTotals = .{};
710     for (manifest.contributions) |contribution| {
711         totals.retained_size += contribution.size;
712         totals.retained_reserve += contribution.reserved_size;
713     }
714     for (manifest.discarded_contributions) |contribution| {
715         totals.discarded_size += contribution.size;
716     }
717     return totals;
718 }
719 
720 fn outputContributionSummary(manifest: Manifest, output_section_name_id: u32) LinkMapContributionSummary {
721     var summary: LinkMapContributionSummary = .{};
722     const output_section_name = manifest.string(output_section_name_id);
723     for (manifest.contributions) |contribution| {
724         if (contribution.output_section_name_id != output_section_name_id and
725             !std.mem.eql(u8, manifest.string(contribution.output_section_name_id), output_section_name)) continue;
726         summary.count += 1;
727         summary.size += contribution.size;
728         summary.reserved_size += contribution.reserved_size;
729     }
730     return summary;
731 }
732 
733 fn discardedReasonSummary(manifest: Manifest, reason: DiscardReason) LinkMapDiscardSummary {
734     var summary: LinkMapDiscardSummary = .{};
735     for (manifest.discarded_contributions) |contribution| {
736         if (contribution.reason != reason) continue;
737         summary.count += 1;
738         summary.size += contribution.size;
739     }
740     return summary;
741 }
742 
743 fn writeBoolBinary(writer: *std.Io.Writer, value: bool) !void {
744     try writer.writeByte(if (value) 1 else 0);
745 }
746 
747 fn writeEnumBinary(writer: *std.Io.Writer, value: anytype) !void {
748     const byte = std.math.cast(u8, @backingInt(value)) orelse return error.ManifestTooLarge;
749     try writer.writeByte(byte);
750 }
751 
752 fn writeCountBinary(writer: *std.Io.Writer, value: usize) !void {
753     const count = std.math.cast(u32, value) orelse return error.ManifestTooLarge;
754     try writeU32Binary(writer, count);
755 }
756 
757 fn writeStringBinary(writer: *std.Io.Writer, value: []const u8) !void {
758     try writeCountBinary(writer, value.len);
759     try writer.writeAll(value);
760 }
761 
762 fn writeU16Binary(writer: *std.Io.Writer, value: u16) !void {
763     var bytes: [2]u8 = undefined;
764     std.mem.writeInt(u16, &bytes, value, .little);
765     try writer.writeAll(&bytes);
766 }
767 
768 fn writeU32Binary(writer: *std.Io.Writer, value: u32) !void {
769     var bytes: [4]u8 = undefined;
770     std.mem.writeInt(u32, &bytes, value, .little);
771     try writer.writeAll(&bytes);
772 }
773 
774 fn writeU64Binary(writer: *std.Io.Writer, value: u64) !void {
775     var bytes: [8]u8 = undefined;
776     std.mem.writeInt(u64, &bytes, value, .little);
777     try writer.writeAll(&bytes);
778 }