|
| 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 | +} |
0 commit comments