Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions src/id.zig
Original file line number Diff line number Diff line change
@@ -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());
}