// Fast allocation-free v4 UUIDs. // Inspired by the Go implementation at github.com/skeeto/uuid const std = @import("std"); const crypto = std.crypto; const fmt = std.fmt; const testing = std.testing; pub const Error = error{InvalidUUID}; pub const UUID = struct { bytes: [16]u8, pub fn init() UUID { var uuid = UUID{ .bytes = undefined }; crypto.random.bytes(&uuid.bytes); // Version 4 uuid.bytes[6] = (uuid.bytes[6] & 0x0f) | 0x40; // Variant 1 uuid.bytes[8] = (uuid.bytes[8] & 0x3f) | 0x80; return uuid; } pub fn compare(self: UUID, other: UUID) bool { return std.meta.eql(self.bytes, other.bytes); } fn to_string(self: UUID, slice: []u8) void { var string: [36]u8 = format_uuid(self); std.mem.copyForwards(u8, slice, &string); } pub fn format_uuid(self: UUID) [36]u8 { var buf: [36]u8 = undefined; buf[8] = '-'; buf[13] = '-'; buf[18] = '-'; buf[23] = '-'; inline for (encoded_pos, 0..) |i, j| { buf[i + 0] = hex[self.bytes[j] >> 4]; buf[i + 1] = hex[self.bytes[j] & 0x0f]; } return buf; } pub fn format_bytes(bytes: [16]u8) [36]u8 { var buf: [36]u8 = undefined; buf[8] = '-'; buf[13] = '-'; buf[18] = '-'; buf[23] = '-'; inline for (encoded_pos, 0..) |i, j| { buf[i + 0] = hex[bytes[j] >> 4]; buf[i + 1] = hex[bytes[j] & 0x0f]; } return buf; } // Indices in the UUID string representation for each byte. const encoded_pos = [16]u8{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34 }; // Hex const hex = "0123456789abcdef"; // Hex to nibble mapping. const hex_to_nibble = [256]u8{ 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, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 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, 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, 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, 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, 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, 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, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }; pub fn format( self: UUID, comptime layout: []const u8, options: fmt.FormatOptions, writer: anytype, ) !void { _ = options; // currently unused if (layout.len != 0 and layout[0] != 's') @compileError("Unsupported format specifier for UUID type: '" ++ layout ++ "'."); const buf = format_uuid(self); try fmt.format(writer, "{s}", .{buf}); } pub fn parse(buf: []const u8) Error!UUID { var uuid = UUID{ .bytes = undefined }; if (buf.len != 36 or buf[8] != '-' or buf[13] != '-' or buf[18] != '-' or buf[23] != '-') return Error.InvalidUUID; inline for (encoded_pos, 0..) |i, j| { const hi = hex_to_nibble[buf[i + 0]]; const lo = hex_to_nibble[buf[i + 1]]; if (hi == 0xff or lo == 0xff) { return Error.InvalidUUID; } uuid.bytes[j] = hi << 4 | lo; } return uuid; } }; // Zero UUID pub const zero: UUID = .{ .bytes = .{0} ** 16 }; // TODO: Optimize both pub fn OR(arr1: *std.ArrayList(UUID), arr2: *std.ArrayList(UUID)) !void { for (0..arr2.items.len) |i| { if (!containUUID(arr1.*, arr2.items[i])) { try arr1.append(arr2.items[i]); } } } pub fn AND(arr1: *std.ArrayList(UUID), arr2: *std.ArrayList(UUID)) !void { var i: usize = 0; for (0..arr1.items.len) |_| { if (!containUUID(arr2.*, arr1.items[i])) { _ = arr1.orderedRemove(i); } else { i += 1; } } } test "OR & AND" { const allocator = std.testing.allocator; var right_arr = std.ArrayList(UUID).init(allocator); defer right_arr.deinit(); try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000005")); try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000006")); try right_arr.append(try UUID.parse("00000000-0000-0000-0000-000000000007")); var left_arr1 = std.ArrayList(UUID).init(allocator); defer left_arr1.deinit(); try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000002")); try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000003")); try left_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000004")); var expected_arr1 = std.ArrayList(UUID).init(allocator); defer expected_arr1.deinit(); try expected_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); try expected_arr1.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); try AND(&left_arr1, &right_arr); try std.testing.expect(compareUUIDArray(left_arr1, expected_arr1)); var left_arr2 = std.ArrayList(UUID).init(allocator); defer left_arr2.deinit(); try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000002")); try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000003")); try left_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000004")); var expected_arr2 = std.ArrayList(UUID).init(allocator); defer expected_arr2.deinit(); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000000")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000001")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000002")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000003")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000004")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000005")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000006")); try expected_arr2.append(try UUID.parse("00000000-0000-0000-0000-000000000007")); try OR(&left_arr2, &right_arr); try std.testing.expect(compareUUIDArray(left_arr2, expected_arr2)); } fn containUUID(arr: std.ArrayList(UUID), value: UUID) bool { return for (arr.items) |elem| { if (value.compare(elem)) break true; } else false; } fn compareUUIDArray(arr1: std.ArrayList(UUID), arr2: std.ArrayList(UUID)) bool { if (arr1.items.len != arr2.items.len) { std.debug.print("Not same array len when comparing UUID. arr1: {d} arr2: {d}\n", .{ arr1.items.len, arr2.items.len }); return false; } for (0..arr1.items.len) |i| { if (!containUUID(arr2, arr1.items[i])) return false; } return true; } test "parse and format" { const uuids = [_][]const u8{ "d0cd8041-0504-40cb-ac8e-d05960d205ec", "3df6f0e4-f9b1-4e34-ad70-33206069b995", "f982cf56-c4ab-4229-b23c-d17377d000be", "6b9f53be-cf46-40e8-8627-6b60dc33def8", "c282ec76-ac18-4d4a-8a29-3b94f5c74813", "00000000-0000-0000-0000-000000000000", }; for (uuids) |uuid| { try testing.expectFmt(uuid, "{}", .{try UUID.parse(uuid)}); } } test "invalid UUID" { const uuids = [_][]const u8{ "3df6f0e4-f9b1-4e34-ad70-33206069b99", // too short "3df6f0e4-f9b1-4e34-ad70-33206069b9912", // too long "3df6f0e4-f9b1-4e34-ad70_33206069b9912", // missing or invalid group separator "zdf6f0e4-f9b1-4e34-ad70-33206069b995", // invalid character }; for (uuids) |uuid| { try testing.expectError(Error.InvalidUUID, UUID.parse(uuid)); } } test "check to_string works" { const uuid1 = UUID.init(); var string1: [36]u8 = undefined; var string2: [36]u8 = undefined; uuid1.to_string(&string1); uuid1.to_string(&string2); try testing.expectEqual(string1, string2); } test "compare" { const uuid1 = UUID.init(); const uuid2 = UUID.init(); try testing.expect(uuid1.compare(uuid1)); try testing.expect(!uuid1.compare(uuid2)); }