diff --git a/src/id.zig b/src/id.zig new file mode 100644 index 00000000..04f16018 --- /dev/null +++ b/src/id.zig @@ -0,0 +1,161 @@ +const std = @import("std"); + +// Generates incrementing prefixed integers, i.e. CTX-1, CTX-2, CTX-3. +// Wraps to 0 on overflow. +// Many caveats for using this: +// - Not thread-safe. +// - Information leaking +// - The slice returned by next() is only valid: +// - while incrementor is valid +// - until the next call to next() +// On the positive, it's zero allocation +fn Incrementing(comptime T: type, comptime prefix: []const u8) type { + // +1 for the '-' separator + const NUMERIC_START = prefix.len + 1; + const MAX_BYTES = NUMERIC_START + switch (T) { + u8 => 3, + u16 => 5, + u32 => 10, + u64 => 20, + else => @compileError("Incrementing must be given an unsigned int type, got: " ++ @typeName(T)), + }; + + const buffer = blk: { + var b = [_]u8{0} ** MAX_BYTES; + @memcpy(b[0..prefix.len], prefix); + b[prefix.len] = '-'; + break :blk b; + }; + + const PrefixIntType = @Type(.{ .Int = .{ + .bits = NUMERIC_START * 8, + .signedness = .unsigned, + } }); + + const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*); + + return struct { + current: T = 0, + buffer: [MAX_BYTES]u8 = buffer, + + const Self = @This(); + + pub fn next(self: *Self) []const u8 { + const current = self.current; + const n = current +% 1; + defer self.current = n; + + const size = std.fmt.formatIntBuf(self.buffer[NUMERIC_START..], n, 10, .lower, .{}); + return self.buffer[0 .. NUMERIC_START + size]; + } + + // extracts the numeric portion from an ID + pub fn parse(str: []const u8) !T { + if (str.len <= NUMERIC_START) { + return error.InvalidId; + } + + if (@as(PrefixIntType, @bitCast(str[0..NUMERIC_START].*)) != PREFIX_INT_CODE) { + return error.InvalidId; + } + + return std.fmt.parseInt(T, str[NUMERIC_START..], 10) catch { + return error.InvalidId; + }; + } + }; +} + +fn uuidv4(hex: []u8) void { + std.debug.assert(hex.len == 36); + + var bin: [16]u8 = undefined; + std.crypto.random.bytes(&bin); + bin[6] = (bin[6] & 0x0f) | 0x40; + bin[8] = (bin[8] & 0x3f) | 0x80; + + const alphabet = "0123456789abcdef"; + + hex[8] = '-'; + hex[13] = '-'; + hex[18] = '-'; + hex[23] = '-'; + + const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 }; + inline for (encoded_pos, 0..) |i, j| { + hex[i + 0] = alphabet[bin[j] >> 4]; + hex[i + 1] = alphabet[bin[j] & 0x0f]; + } +} + +const hex_to_nibble = [_]u8{0xff} ** 48 ++ [_]u8{ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0xff, +} ++ [_]u8{0xff} ** 152; + +const testing = std.testing; +test "id: Incrementing.next" { + var id = Incrementing(u16, "IDX"){}; + try testing.expectEqualStrings("IDX-1", id.next()); + try testing.expectEqualStrings("IDX-2", id.next()); + try testing.expectEqualStrings("IDX-3", id.next()); + + // force a wrap + id.current = 65533; + try testing.expectEqualStrings("IDX-65534", id.next()); + try testing.expectEqualStrings("IDX-65535", id.next()); + try testing.expectEqualStrings("IDX-0", id.next()); +} + +test "id: Incrementing.parse" { + const ReqId = Incrementing(u32, "REQ"); + try testing.expectError(error.InvalidId, ReqId.parse("")); + try testing.expectError(error.InvalidId, ReqId.parse("R")); + try testing.expectError(error.InvalidId, ReqId.parse("RE")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ-")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ--1")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ--")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ-Nope")); + try testing.expectError(error.InvalidId, ReqId.parse("REQ-4294967296")); + + try testing.expectEqual(0, try ReqId.parse("REQ-0")); + try testing.expectEqual(99, try ReqId.parse("REQ-99")); + try testing.expectEqual(4294967295, try ReqId.parse("REQ-4294967295")); +} + +test "id: uuiv4" { + const expectUUID = struct { + fn expect(uuid: [36]u8) !void { + for (uuid, 0..) |b, i| { + switch (b) { + '0'...'9', 'a'...'z' => {}, + '-' => { + if (i != 8 and i != 13 and i != 18 and i != 23) { + return error.InvalidEncoding; + } + }, + else => return error.InvalidHexEncoding, + } + } + } + }.expect; + + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var seen = std.StringHashMapUnmanaged(void){}; + for (0..100) |_| { + var hex: [36]u8 = undefined; + uuidv4(&hex); + try expectUUID(hex); + try seen.put(allocator, try allocator.dupe(u8, &hex), {}); + } + try testing.expectEqual(100, seen.count()); +}