Skip to content

Commit 21bd475

Browse files
committed
Add an id generator
Create UUID v4. Create prefixed ids. To support more of the CDP protocol, we need to remove the hard-coded IDs (session, browser context, frame, loader, ...) and be able to dynamically create them, i.e. creating a new BrowserContextId when Target.createBrowserContext is called. var frame_id = id.Incremental(u16, "FRM"){}; frame_id.next() == "FRM-1" frame_id.next() == "FRM-2" Generation is allocation-free (the returned string is only valid until the next call to next()). This is not thread safe, each CDP instance will have its own generator (for each id it needs to generate). The generated IDs are different than what Chrome uses, i.e. BROWSERSESSIONID597D9875C664CAC0. I looked at various drivers and none have any expectations beyond a string. Shorter IDs will be more efficient. Also, the ID can cheeply be converted to and from an integer, allowing for lookups via AutoHashMap(u16) instead of StringHashMap.
1 parent 09505db commit 21bd475

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

src/id.zig

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
const std = @import("std");
2+
3+
// Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3.
4+
// Wraps to 0 on overflow.
5+
// Many caveats for using this:
6+
// - Not thread-safe.
7+
// - Information leaking
8+
// - The slice returned by next() is only valid:
9+
// - while incrementor is valid
10+
// - until the next call to next()
11+
// On the positive, it's zero allocation
12+
fn Incrementing(comptime T: type, comptime prefix: []const u8) type {
13+
// +1 for the '-' separator
14+
const NUMERIC_START = prefix.len + 1;
15+
const MAX_BYTES = NUMERIC_START + switch (T) {
16+
u8 => 3,
17+
u16 => 5,
18+
u32 => 10,
19+
u64 => 20,
20+
else => @compileError("Incrementing must be given an unsigned int type, got: " ++ @typeName(T)),
21+
};
22+
23+
const buffer = blk: {
24+
var b = [_]u8{0} ** MAX_BYTES;
25+
@memcpy(b[0..prefix.len], prefix);
26+
b[prefix.len] = '-';
27+
break :blk b;
28+
};
29+
30+
const PrefixIntType = @Type(.{ .Int = .{
31+
.bits = NUMERIC_START * 8,
32+
.signedness = .unsigned,
33+
} });
34+
35+
const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*);
36+
37+
return struct {
38+
current: T = 0,
39+
buffer: [MAX_BYTES]u8 = buffer,
40+
41+
const Self = @This();
42+
43+
pub fn next(self: *Self) []const u8 {
44+
const current = self.current;
45+
const n = current +% 1;
46+
defer self.current = n;
47+
48+
const size = std.fmt.formatIntBuf(self.buffer[NUMERIC_START..], n, 10, .lower, .{});
49+
return self.buffer[0 .. NUMERIC_START + size];
50+
}
51+
52+
// extracts the numeric portion from an ID
53+
pub fn parse(str: []const u8) !T {
54+
if (str.len <= NUMERIC_START) {
55+
return error.InvalidId;
56+
}
57+
58+
if (@as(PrefixIntType, @bitCast(str[0..NUMERIC_START].*)) != PREFIX_INT_CODE) {
59+
return error.InvalidId;
60+
}
61+
62+
return std.fmt.parseInt(T, str[NUMERIC_START..], 10) catch {
63+
return error.InvalidId;
64+
};
65+
}
66+
};
67+
}
68+
69+
fn uuidv4(hex: []u8) void {
70+
std.debug.assert(hex.len == 36);
71+
72+
var bin: [16]u8 = undefined;
73+
std.crypto.random.bytes(&bin);
74+
bin[6] = (bin[6] & 0x0f) | 0x40;
75+
bin[8] = (bin[8] & 0x3f) | 0x80;
76+
77+
const alphabet = "0123456789abcdef";
78+
79+
hex[8] = '-';
80+
hex[13] = '-';
81+
hex[18] = '-';
82+
hex[23] = '-';
83+
84+
const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 };
85+
inline for (encoded_pos, 0..) |i, j| {
86+
hex[i + 0] = alphabet[bin[j] >> 4];
87+
hex[i + 1] = alphabet[bin[j] & 0x0f];
88+
}
89+
}
90+
91+
const hex_to_nibble = [_]u8{0xff} ** 48 ++ [_]u8{
92+
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
93+
0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
94+
0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
95+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
96+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
97+
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
98+
0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff,
99+
} ++ [_]u8{0xff} ** 152;
100+
101+
const testing = std.testing;
102+
test "id: Incrementing.next" {
103+
var id = Incrementing(u16, "IDX"){};
104+
try testing.expectEqualStrings("IDX-1", id.next());
105+
try testing.expectEqualStrings("IDX-2", id.next());
106+
try testing.expectEqualStrings("IDX-3", id.next());
107+
108+
// force a wrap
109+
id.current = 65533;
110+
try testing.expectEqualStrings("IDX-65534", id.next());
111+
try testing.expectEqualStrings("IDX-65535", id.next());
112+
try testing.expectEqualStrings("IDX-0", id.next());
113+
}
114+
115+
test "id: Incrementing.parse" {
116+
const ReqId = Incrementing(u32, "REQ");
117+
try testing.expectError(error.InvalidId, ReqId.parse(""));
118+
try testing.expectError(error.InvalidId, ReqId.parse("R"));
119+
try testing.expectError(error.InvalidId, ReqId.parse("RE"));
120+
try testing.expectError(error.InvalidId, ReqId.parse("REQ"));
121+
try testing.expectError(error.InvalidId, ReqId.parse("REQ-"));
122+
try testing.expectError(error.InvalidId, ReqId.parse("REQ--1"));
123+
try testing.expectError(error.InvalidId, ReqId.parse("REQ--"));
124+
try testing.expectError(error.InvalidId, ReqId.parse("REQ-Nope"));
125+
try testing.expectError(error.InvalidId, ReqId.parse("REQ-4294967296"));
126+
127+
try testing.expectEqual(0, try ReqId.parse("REQ-0"));
128+
try testing.expectEqual(99, try ReqId.parse("REQ-99"));
129+
try testing.expectEqual(4294967295, try ReqId.parse("REQ-4294967295"));
130+
}
131+
132+
test "id: uuiv4" {
133+
const expectUUID = struct {
134+
fn expect(uuid: [36]u8) !void {
135+
for (uuid, 0..) |b, i| {
136+
switch (b) {
137+
'0'...'9', 'a'...'z' => {},
138+
'-' => {
139+
if (i != 8 and i != 13 and i != 18 and i != 23) {
140+
return error.InvalidEncoding;
141+
}
142+
},
143+
else => return error.InvalidHexEncoding,
144+
}
145+
}
146+
}
147+
}.expect;
148+
149+
var arena = std.heap.ArenaAllocator.init(testing.allocator);
150+
defer arena.deinit();
151+
const allocator = arena.allocator();
152+
153+
var seen = std.StringHashMapUnmanaged(void){};
154+
for (0..100) |_| {
155+
var hex: [36]u8 = undefined;
156+
uuidv4(&hex);
157+
try expectUUID(hex);
158+
try seen.put(allocator, try allocator.dupe(u8, &hex), {});
159+
}
160+
try testing.expectEqual(100, seen.count());
161+
}

src/unit_tests.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,5 @@ test {
377377
std.testing.refAllDecls(@import("storage/storage.zig"));
378378
std.testing.refAllDecls(@import("iterator/iterator.zig"));
379379
std.testing.refAllDecls(@import("server.zig"));
380+
std.testing.refAllDecls(@import("id.zig"));
380381
}

0 commit comments

Comments
 (0)