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 }