lib/coz/src/files.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const Index = struct {
4 entries: std.StringHashMapUnmanaged(usize) = .empty,
5
6 pub fn deinit(self: *Index, allocator: std.mem.Allocator) void {
7 var iter = self.entries.keyIterator();
8 while (iter.next()) |key| allocator.free(key.*);
9 self.entries.deinit(allocator);
10 self.* = .{};
11 }
12
13 pub fn count(self: *const Index) usize {
14 return self.entries.count();
15 }
16
17 pub fn get(self: *const Index, path: []const u8) ?usize {
18 return self.entries.get(path);
19 }
20
21 pub fn put(self: *Index, allocator: std.mem.Allocator, path: []const u8, load_address: usize) !void {
22 if (self.entries.getPtr(path)) |existing| {
23 existing.* = load_address;
24 return;
25 }
26
27 const owned_path = try allocator.dupe(u8, path);
28 errdefer allocator.free(owned_path);
29 try self.entries.put(allocator, owned_path, load_address);
30 }
31 };
32
33 pub fn parseProcMaps(allocator: std.mem.Allocator, text: []const u8) !Index {
34 var result: Index = .{};
35 errdefer result.deinit(allocator);
36
37 var lines = std.mem.splitScalar(u8, text, '\n');
38 while (lines.next()) |line| {
39 try parseProcMapsLine(allocator, &result, line);
40 }
41
42 return result;
43 }
44
45 fn parseProcMapsLine(allocator: std.mem.Allocator, result: *Index, raw_line: []const u8) !void {
46 const line = trimLeft(raw_line, " \t\r");
47 if (line.len == 0) return;
48
49 var cursor: usize = 0;
50 const address_field = nextField(line, &cursor) orelse return error.InvalidProcMapsLine;
51 const perms = nextField(line, &cursor) orelse return error.InvalidProcMapsLine;
52 const offset_field = nextField(line, &cursor) orelse return error.InvalidProcMapsLine;
53 const dev = nextField(line, &cursor) orelse return error.InvalidProcMapsLine;
54 const inode = nextField(line, &cursor) orelse return error.InvalidProcMapsLine;
55 const path = trimRight(trimLeft(line[cursor..], " \t"), "\r");
56
57 if (std.mem.indexOfScalar(u8, dev, ':') == null) return error.InvalidProcMapsLine;
58 _ = try parseDecimal(u64, inode);
59
60 if (perms.len < 3 or perms[2] != 'x') return;
61 if (path.len == 0 or path[0] != '/') return;
62
63 const dash = std.mem.indexOfScalar(u8, address_field, '-') orelse return error.InvalidProcMapsLine;
64 const base = try parseHex(usize, address_field[0..dash]);
65 _ = try parseHex(usize, address_field[dash + 1 ..]);
66 const offset = try parseHex(usize, offset_field);
67 const load_address = std.math.sub(usize, base, offset) catch return error.InvalidMappingAddress;
68
69 try result.put(allocator, path, load_address);
70 }
71
72 fn nextField(line: []const u8, cursor: *usize) ?[]const u8 {
73 while (cursor.* < line.len and isSpace(line[cursor.*])) cursor.* += 1;
74 if (cursor.* >= line.len) return null;
75
76 const start = cursor.*;
77 while (cursor.* < line.len and !isSpace(line[cursor.*])) cursor.* += 1;
78 return line[start..cursor.*];
79 }
80
81 fn isSpace(byte: u8) bool {
82 return byte == ' ' or byte == '\t';
83 }
84
85 fn parseHex(comptime T: type, text: []const u8) !T {
86 return std.fmt.parseUnsigned(T, text, 16) catch return error.InvalidProcMapsLine;
87 }
88
89 fn parseDecimal(comptime T: type, text: []const u8) !T {
90 return std.fmt.parseUnsigned(T, text, 10) catch return error.InvalidProcMapsLine;
91 }
92
93 fn trimLeft(bytes: []const u8, values: []const u8) []const u8 {
94 var start: usize = 0;
95 while (start < bytes.len and std.mem.indexOfScalar(u8, values, bytes[start]) != null) start += 1;
96 return bytes[start..];
97 }
98
99 fn trimRight(bytes: []const u8, values: []const u8) []const u8 {
100 var end = bytes.len;
101 while (end > 0 and std.mem.indexOfScalar(u8, values, bytes[end - 1]) != null) end -= 1;
102 return bytes[0..end];
103 }
104
105 test "proc maps parser keeps executable absolute mappings" {
106 const text =
107 \\00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/app
108 \\00652000-00653000 r--p 00052000 08:02 173521 /usr/bin/app
109 \\7f0000001000-7f0000011000 r-xp 00002000 08:02 222222 /lib/libc.so.6
110 \\7f0000011000-7f0000012000 rw-p 00012000 08:02 222222 /lib/libc.so.6
111 \\
112 ;
113
114 var files = try parseProcMaps(std.testing.allocator, text);
115 defer files.deinit(std.testing.allocator);
116
117 try std.testing.expectEqual(@as(usize, 2), files.count());
118 try std.testing.expectEqual(@as(usize, 0x00400000), files.get("/usr/bin/app").?);
119 try std.testing.expectEqual(@as(usize, 0x7f0000001000 - 0x2000), files.get("/lib/libc.so.6").?);
120 }
121
122 test "proc maps parser ignores nonabsolute and nonexecutable mappings" {
123 const text =
124 \\00100000-00110000 rw-p 00000000 00:00 0 /tmp/data
125 \\00200000-00210000 r-xp 00000000 00:00 0 [vdso]
126 \\00300000-00310000 r-xp 00000000 00:00 0 relative/path
127 \\
128 ;
129
130 var files = try parseProcMaps(std.testing.allocator, text);
131 defer files.deinit(std.testing.allocator);
132
133 try std.testing.expectEqual(@as(usize, 0), files.count());
134 }
135
136 test "proc maps parser preserves path suffixes" {
137 const text =
138 \\00001000-00002000 r-xp 00000000 00:00 1 /tmp/name with space (deleted)
139 \\
140 ;
141
142 var files = try parseProcMaps(std.testing.allocator, text);
143 defer files.deinit(std.testing.allocator);
144
145 try std.testing.expectEqual(@as(usize, 1), files.count());
146 try std.testing.expectEqual(@as(usize, 0x1000), files.get("/tmp/name with space (deleted)").?);
147 }
148
149 test "proc maps parser uses the latest mapping for duplicate paths" {
150 const text =
151 \\00001000-00002000 r-xp 00000000 00:00 1 /bin/app
152 \\00003000-00004000 r-xp 00001000 00:00 1 /bin/app
153 \\
154 ;
155
156 var files = try parseProcMaps(std.testing.allocator, text);
157 defer files.deinit(std.testing.allocator);
158
159 try std.testing.expectEqual(@as(usize, 1), files.count());
160 try std.testing.expectEqual(@as(usize, 0x2000), files.get("/bin/app").?);
161 }
162
163 test "proc maps parser rejects malformed mapping lines" {
164 const text = "00001000-00002000 r-xp 00000000 00:00 /bin/app\n";
165
166 try std.testing.expectEqual(error.InvalidProcMapsLine, parseProcMaps(std.testing.allocator, text));
167 }