Split util into own package
This commit is contained in:
parent
407923eacd
commit
0f7754802d
10 changed files with 240 additions and 469 deletions
12
build.zig
12
build.zig
|
@ -1,5 +1,9 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const static_libs = [_][]const u8{
|
||||||
|
"util",
|
||||||
|
};
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) void {
|
pub fn build(b: *std.build.Builder) void {
|
||||||
// Standard target options allows the person running `zig build` to choose
|
// Standard target options allows the person running `zig build` to choose
|
||||||
// what target to build for. Here we do not override the defaults, which
|
// what target to build for. Here we do not override the defaults, which
|
||||||
|
@ -11,9 +15,15 @@ pub fn build(b: *std.build.Builder) void {
|
||||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
|
||||||
const mode = b.standardReleaseOptions();
|
const mode = b.standardReleaseOptions();
|
||||||
|
|
||||||
const exe = b.addExecutable("apub", "src/main.zig");
|
const exe = b.addExecutable("apub", "src/main/main.zig");
|
||||||
exe.setTarget(target);
|
exe.setTarget(target);
|
||||||
exe.setBuildMode(mode);
|
exe.setBuildMode(mode);
|
||||||
|
|
||||||
|
inline for (static_libs) |name| {
|
||||||
|
const lib = b.addStaticLibrary(name, "src/" ++ name ++ "/lib.zig");
|
||||||
|
exe.linkLibrary(lib);
|
||||||
|
}
|
||||||
|
|
||||||
exe.install();
|
exe.install();
|
||||||
|
|
||||||
const run_cmd = exe.run();
|
const run_cmd = exe.run();
|
||||||
|
|
|
@ -1,230 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const util = @import("../util.zig");
|
|
||||||
const http = @import("../http.zig");
|
|
||||||
|
|
||||||
const Method = http.Method;
|
|
||||||
const ciutf8 = util.ciutf8;
|
|
||||||
|
|
||||||
pub fn Router(comptime Context: type) type {
|
|
||||||
return struct {
|
|
||||||
const Self = @This();
|
|
||||||
routes: []const Route,
|
|
||||||
|
|
||||||
pub const Route = struct {
|
|
||||||
const Segment = union(enum) {
|
|
||||||
param: []const u8,
|
|
||||||
literal: []const u8,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Handler = fn (*Context, *const Route) callconv(.Async) anyerror!void;
|
|
||||||
fn normalize(comptime path: []const u8) []const u8 {
|
|
||||||
var arr: [path.len]u8 = undefined;
|
|
||||||
|
|
||||||
comptime var i = 0;
|
|
||||||
for (path) |ch| {
|
|
||||||
if (i == 0 and ch == '/') continue;
|
|
||||||
if (i > 0 and ch == '/' and arr[i - 1] == '/') continue;
|
|
||||||
|
|
||||||
arr[i] = ch;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i > 0 and arr[i - 1] == '/') {
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr[0..i];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseSegments(comptime path: []const u8) []const Segment {
|
|
||||||
var count = 1;
|
|
||||||
for (path) |ch| {
|
|
||||||
if (ch == '/') count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var segment_array: [count]Segment = undefined;
|
|
||||||
|
|
||||||
var segment_start = 0;
|
|
||||||
for (segment_array) |*seg| {
|
|
||||||
var index = segment_start;
|
|
||||||
while (index < path.len) : (index += 1) {
|
|
||||||
if (path[index] == '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const slice = path[segment_start..index];
|
|
||||||
if (slice.len > 0 and slice[0] == ':') {
|
|
||||||
// doing this kinda jankily to get around segfaults in compiler
|
|
||||||
const param = path[segment_start + 1 .. index];
|
|
||||||
seg.* = .{ .param = param };
|
|
||||||
} else {
|
|
||||||
seg.* = .{ .literal = slice };
|
|
||||||
}
|
|
||||||
|
|
||||||
segment_start = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return &segment_array;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from(method: Method, comptime path: []const u8, handler: Handler) Route {
|
|
||||||
const segments = parseSegments(normalize(path));
|
|
||||||
return Route{ .method = method, .path = segments, .handler = handler };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nextSegment(path: []const u8) ?[]const u8 {
|
|
||||||
var start: usize = 0;
|
|
||||||
var end: usize = start;
|
|
||||||
while (end < path.len) : (end += 1) {
|
|
||||||
// skip leading slash
|
|
||||||
if (end == start and path[start] == '/') {
|
|
||||||
start += 1;
|
|
||||||
continue;
|
|
||||||
} else if (path[end] == '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start == end) return null;
|
|
||||||
|
|
||||||
return path[start..end];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn matches(self: Route, path: []const u8) bool {
|
|
||||||
var segment_start: usize = 0;
|
|
||||||
for (self.path) |seg| {
|
|
||||||
var index = segment_start;
|
|
||||||
while (index < path.len) : (index += 1) {
|
|
||||||
// skip leading slash
|
|
||||||
if (index == segment_start and path[index] == '/') {
|
|
||||||
segment_start += 1;
|
|
||||||
continue;
|
|
||||||
} else if (path[index] == '/') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const slice = path[segment_start..index];
|
|
||||||
const match = switch (seg) {
|
|
||||||
.literal => |str| ciutf8.eql(slice, str),
|
|
||||||
.param => true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!match) return false;
|
|
||||||
|
|
||||||
segment_start = index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for trailing path
|
|
||||||
while (segment_start < path.len) : (segment_start += 1) {
|
|
||||||
if (path[segment_start] != '/') return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn arg(self: Route, name: []const u8, path: []const u8) []const u8 {
|
|
||||||
var index: usize = 0;
|
|
||||||
for (self.path) |seg| {
|
|
||||||
const slice = nextSegment(path[index..]);
|
|
||||||
if (slice == null) return "";
|
|
||||||
|
|
||||||
index = @ptrToInt(slice.?.ptr) - @ptrToInt(path.ptr) + slice.?.len + 1;
|
|
||||||
|
|
||||||
switch (seg) {
|
|
||||||
.param => |param| {
|
|
||||||
if (std.mem.eql(u8, param, name)) {
|
|
||||||
return slice.?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
.literal => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std.log.err("unknown parameter {s}", .{name});
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
method: Method,
|
|
||||||
path: []const Segment,
|
|
||||||
handler: Handler,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn handleNotFound(ctx: *Context) !void {
|
|
||||||
try ctx.response.writer.writeAll("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn routeRequest(self: Self, ctx: *Context) !void {
|
|
||||||
for (self.routes) |*route| {
|
|
||||||
if (route.method == ctx.request.method and route.matches(ctx.request.path)) {
|
|
||||||
std.log.info("{s} {s}", .{ @tagName(ctx.request.method), ctx.request.path });
|
|
||||||
//ctx.request.route = route;
|
|
||||||
|
|
||||||
var buf = try ctx.allocator.allocWithOptions(u8, @frameSize(route.handler), 8, null);
|
|
||||||
defer ctx.allocator.free(buf);
|
|
||||||
return await @asyncCall(buf, {}, route.handler, .{ ctx, route });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std.log.info("404 {s} {s}", .{ @tagName(ctx.request.method), ctx.request.path });
|
|
||||||
try handleNotFound(ctx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TestContext = struct {
|
|
||||||
request: struct {
|
|
||||||
method: Method,
|
|
||||||
path: []const u8,
|
|
||||||
},
|
|
||||||
allocator: std.mem.Allocator = std.testing.allocator,
|
|
||||||
};
|
|
||||||
const TestRouter = Router(TestContext);
|
|
||||||
const TestRoute = TestRouter.Route;
|
|
||||||
|
|
||||||
fn CallTracker(comptime _uniq: anytype, comptime next: TestRoute.Handler) type {
|
|
||||||
_ = _uniq;
|
|
||||||
return struct {
|
|
||||||
var calls: u32 = 0;
|
|
||||||
fn func(ctx: *TestContext, route: *const TestRoute) !void {
|
|
||||||
calls += 1;
|
|
||||||
return next(ctx, route);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expectCalled(times: u32) !void {
|
|
||||||
return std.testing.expectEqual(times, calls);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset() void {
|
|
||||||
calls = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expectNotCalled(_: *TestContext, _: *const TestRoute) !void {
|
|
||||||
return error.TestWrongRouteChosen;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dummyHandler(_: *TestContext, _: *const TestRoute) !void {}
|
|
||||||
|
|
||||||
test "routeRequest" {
|
|
||||||
const call_tracker = CallTracker(.{}, dummyHandler);
|
|
||||||
const test_routes = [_]TestRoute{
|
|
||||||
TestRoute.from(.GET, "/ab", expectNotCalled),
|
|
||||||
TestRoute.from(.GET, "/abc", call_tracker.func),
|
|
||||||
TestRoute.from(.GET, "/abcdefg", expectNotCalled),
|
|
||||||
TestRoute.from(.GET, "/", expectNotCalled),
|
|
||||||
};
|
|
||||||
var context = TestContext{
|
|
||||||
.request = .{
|
|
||||||
.method = .GET,
|
|
||||||
.path = "/abc",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try (TestRouter{ .routes = &test_routes }).routeRequest(&context);
|
|
||||||
|
|
||||||
try call_tracker.expectCalled(1);
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const util = @import("util");
|
||||||
|
|
||||||
pub const db = @import("./db.zig");
|
pub const db = @import("./db.zig");
|
||||||
pub const util = @import("./util.zig");
|
|
||||||
pub const http = @import("./http.zig");
|
pub const http = @import("./http.zig");
|
||||||
pub const routing = @import("./routing.zig");
|
pub const routing = @import("./routing.zig");
|
||||||
|
|
237
src/util.zig
237
src/util.zig
|
@ -1,237 +0,0 @@
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
threadlocal var tls_prng: ?std.rand.DefaultPrng = null;
|
|
||||||
|
|
||||||
fn initPrng() void {
|
|
||||||
const higher_seed = (@bitCast(u64, std.Thread.getCurrentId()) & 0xffffffff) << 32;
|
|
||||||
const lower_seed = @bitCast(u64, std.time.milliTimestamp()) & 0xffffffff;
|
|
||||||
|
|
||||||
tls_prng = std.rand.DefaultPrng.init(higher_seed | lower_seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getRandom() std.rand.Random {
|
|
||||||
if (tls_prng) |*prng| {
|
|
||||||
return prng.random();
|
|
||||||
}
|
|
||||||
|
|
||||||
initPrng();
|
|
||||||
return tls_prng.?.random();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const Uuid = struct {
|
|
||||||
data: [16]u8,
|
|
||||||
|
|
||||||
pub const Nil = Uuid{ .data = @bitCast([16]u8, @as(u128, 0)) };
|
|
||||||
pub const StringLen = 36;
|
|
||||||
|
|
||||||
pub fn eql(lhs: Uuid, rhs: Uuid) bool {
|
|
||||||
return std.mem.eql(u8, &lhs.data, &rhs.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format(value: Uuid, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
|
||||||
try std.fmt.format(writer, "{x:0>2}{x:0>2}{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}", .{
|
|
||||||
value.data[0],
|
|
||||||
value.data[1],
|
|
||||||
value.data[2],
|
|
||||||
value.data[3],
|
|
||||||
value.data[4],
|
|
||||||
value.data[5],
|
|
||||||
value.data[6],
|
|
||||||
value.data[7],
|
|
||||||
value.data[8],
|
|
||||||
value.data[9],
|
|
||||||
value.data[10],
|
|
||||||
value.data[11],
|
|
||||||
value.data[12],
|
|
||||||
value.data[13],
|
|
||||||
value.data[14],
|
|
||||||
value.data[15],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const ParseError = error{
|
|
||||||
InvalidCharacter,
|
|
||||||
InvalidLength,
|
|
||||||
};
|
|
||||||
pub fn parse(str: []const u8) ParseError!Uuid {
|
|
||||||
if (str.len != StringLen) return error.InvalidLength;
|
|
||||||
|
|
||||||
var uuid: Uuid = undefined;
|
|
||||||
var str_i: usize = 0;
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < 16 and str_i < str.len) : ({
|
|
||||||
i += 1;
|
|
||||||
str_i += 2;
|
|
||||||
}) {
|
|
||||||
uuid.data[i] = std.fmt.parseInt(u8, str[str_i .. str_i + 2], 16) catch |err| switch (err) {
|
|
||||||
error.InvalidCharacter => return error.InvalidCharacter,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (i == 3 or i == 5 or i == 7 or i == 9) {
|
|
||||||
if (str[str_i + 2] != '-') return error.InvalidCharacter;
|
|
||||||
str_i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn randV4(rand: std.rand.Random) Uuid {
|
|
||||||
var ret: Uuid = undefined;
|
|
||||||
rand.bytes(&ret.data);
|
|
||||||
|
|
||||||
// signify that this is a random v4 uuid
|
|
||||||
ret.data[7] = (0b0100_0000) | (ret.data[7] & 0b1111);
|
|
||||||
ret.data[9] = (0b1000_0000) | (ret.data[9] & 0b11_1111);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "parse uuid" {
|
|
||||||
try std.testing.expectEqual(
|
|
||||||
Uuid.Nil,
|
|
||||||
try Uuid.parse("00000000-0000-0000-0000-000000000000"),
|
|
||||||
);
|
|
||||||
|
|
||||||
try std.testing.expectEqual(
|
|
||||||
Uuid{
|
|
||||||
.data = @bitCast([16]u8, @as(u128, 0x4ba7b74522ad_1da8_c242_d312_60515ff7)),
|
|
||||||
},
|
|
||||||
try Uuid.parse("f75f5160-12d3-42c2-a81d-ad2245b7a74b"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
test "format uuid" {
|
|
||||||
try std.testing.expectFmt("00000000-0000-0000-0000-000000000000", "{}", .{Uuid.Nil});
|
|
||||||
|
|
||||||
const uuid = Uuid{
|
|
||||||
.data = @bitCast([16]u8, @as(u128, 0x4ba7b74522ad_1da8_c242_d312_60515ff7)),
|
|
||||||
};
|
|
||||||
try std.testing.expectFmt("f75f5160-12d3-42c2-a81d-ad2245b7a74b", "{}", .{uuid});
|
|
||||||
|
|
||||||
try std.testing.expectError(error.InvalidLength, Uuid.parse("fsdfs"));
|
|
||||||
try std.testing.expectError(error.InvalidCharacter, Uuid.parse("00000000-0000-0000-xxxx-000000000000"));
|
|
||||||
try std.testing.expectError(error.InvalidLength, Uuid.parse("00000000-0000-0000-0000-000000000000fsdfs"));
|
|
||||||
try std.testing.expectError(error.InvalidCharacter, Uuid.parse("00000000-0000x0000-0000-000000000000"));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "roundtrip random uuid" {
|
|
||||||
const uuid = Uuid.randV4(getRandom());
|
|
||||||
|
|
||||||
var buf: [36]u8 = undefined;
|
|
||||||
var fbs = std.io.fixedBufferStream(&buf);
|
|
||||||
try Uuid.format(uuid, "", .{}, fbs.writer());
|
|
||||||
|
|
||||||
const parsed = try Uuid.parse(&buf);
|
|
||||||
|
|
||||||
try std.testing.expectEqual(uuid, parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const ciutf8 = struct {
|
|
||||||
const Hash = std.hash.Wyhash;
|
|
||||||
const View = std.unicode.Utf8View;
|
|
||||||
const toLower = std.ascii.toLower;
|
|
||||||
const isAscii = std.ascii.isASCII;
|
|
||||||
const seed = 1;
|
|
||||||
|
|
||||||
pub fn hash(str: []const u8) u64 {
|
|
||||||
// fallback to regular hash on invalid utf8
|
|
||||||
const view = View.init(str) catch return Hash.hash(seed, str);
|
|
||||||
var iter = view.iterator();
|
|
||||||
|
|
||||||
var h = Hash.init(seed);
|
|
||||||
|
|
||||||
var it = iter.nextCodepointSlice();
|
|
||||||
while (it != null) : (it = iter.nextCodepointSlice()) {
|
|
||||||
if (it.?.len == 1 and isAscii(it.?[0])) {
|
|
||||||
const ch = [1]u8{toLower(it.?[0])};
|
|
||||||
h.update(&ch);
|
|
||||||
} else {
|
|
||||||
h.update(it.?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.final();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eql(a: []const u8, b: []const u8) bool {
|
|
||||||
if (a.len != b.len) return false;
|
|
||||||
|
|
||||||
const va = View.init(a) catch return std.mem.eql(u8, a, b);
|
|
||||||
const vb = View.init(b) catch return false;
|
|
||||||
|
|
||||||
var iter_a = va.iterator();
|
|
||||||
var iter_b = vb.iterator();
|
|
||||||
|
|
||||||
var it_a = iter_a.nextCodepointSlice();
|
|
||||||
var it_b = iter_b.nextCodepointSlice();
|
|
||||||
|
|
||||||
while (it_a != null and it_b != null) : ({
|
|
||||||
it_a = iter_a.nextCodepointSlice();
|
|
||||||
it_b = iter_b.nextCodepointSlice();
|
|
||||||
}) {
|
|
||||||
if (it_a.?.len != it_b.?.len) return false;
|
|
||||||
|
|
||||||
if (it_a.?.len == 1) {
|
|
||||||
if (isAscii(it_a.?[0]) and isAscii(it_b.?[0])) {
|
|
||||||
const ch_a = toLower(it_a.?[0]);
|
|
||||||
const ch_b = toLower(it_b.?[0]);
|
|
||||||
|
|
||||||
if (ch_a != ch_b) return false;
|
|
||||||
} else if (it_a.?[0] != it_b.?[0]) return false;
|
|
||||||
} else if (!std.mem.eql(u8, it_a.?, it_b.?)) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return it_a == null and it_b == null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
test "case insensitive eql with utf-8 chars" {
|
|
||||||
const t = std.testing;
|
|
||||||
try t.expectEqual(true, ciutf8.eql("abc 💯 def", "aBc 💯 DEF"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("xyz 💯 ijk", "aBc 💯 DEF"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("abc 💯 def", "aBc x DEF"));
|
|
||||||
try t.expectEqual(true, ciutf8.eql("💯", "💯"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("💯", "a"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("💯", "💯 continues"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("💯 fsdfs", "💯"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("💯", ""));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("", "💯"));
|
|
||||||
|
|
||||||
try t.expectEqual(true, ciutf8.eql("abc x def", "aBc x DEF"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("xyz x ijk", "aBc x DEF"));
|
|
||||||
try t.expectEqual(true, ciutf8.eql("x", "x"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("x", "a"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("x", "x continues"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("x fsdfs", "x"));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("x", ""));
|
|
||||||
try t.expectEqual(false, ciutf8.eql("", "x"));
|
|
||||||
|
|
||||||
try t.expectEqual(true, ciutf8.eql("", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
test "case insensitive hash with utf-8 chars" {
|
|
||||||
const t = std.testing;
|
|
||||||
try t.expect(ciutf8.hash("abc 💯 def") == ciutf8.hash("aBc 💯 DEF"));
|
|
||||||
try t.expect(ciutf8.hash("xyz 💯 ijk") != ciutf8.hash("aBc 💯 DEF"));
|
|
||||||
try t.expect(ciutf8.hash("abc 💯 def") != ciutf8.hash("aBc x DEF"));
|
|
||||||
try t.expect(ciutf8.hash("💯") == ciutf8.hash("💯"));
|
|
||||||
try t.expect(ciutf8.hash("💯") != ciutf8.hash("a"));
|
|
||||||
try t.expect(ciutf8.hash("💯") != ciutf8.hash("💯 continues"));
|
|
||||||
try t.expect(ciutf8.hash("💯 fsdfs") != ciutf8.hash("💯"));
|
|
||||||
try t.expect(ciutf8.hash("💯") != ciutf8.hash(""));
|
|
||||||
try t.expect(ciutf8.hash("") != ciutf8.hash("💯"));
|
|
||||||
|
|
||||||
try t.expect(ciutf8.hash("abc x def") == ciutf8.hash("aBc x DEF"));
|
|
||||||
try t.expect(ciutf8.hash("xyz x ijk") != ciutf8.hash("aBc x DEF"));
|
|
||||||
try t.expect(ciutf8.hash("x") == ciutf8.hash("x"));
|
|
||||||
try t.expect(ciutf8.hash("x") != ciutf8.hash("a"));
|
|
||||||
try t.expect(ciutf8.hash("x") != ciutf8.hash("x continues"));
|
|
||||||
try t.expect(ciutf8.hash("x fsdfs") != ciutf8.hash("x"));
|
|
||||||
try t.expect(ciutf8.hash("x") != ciutf8.hash(""));
|
|
||||||
try t.expect(ciutf8.hash("") != ciutf8.hash("x"));
|
|
||||||
|
|
||||||
try t.expect(ciutf8.hash("") == ciutf8.hash(""));
|
|
||||||
}
|
|
115
src/util/Uuid.zig
Normal file
115
src/util/Uuid.zig
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
const Uuid = @This();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
data: [16]u8,
|
||||||
|
|
||||||
|
pub const Nil = Uuid{ .data = @bitCast([16]u8, @as(u128, 0)) };
|
||||||
|
pub const StringLen = 36;
|
||||||
|
|
||||||
|
pub fn eql(lhs: Uuid, rhs: Uuid) bool {
|
||||||
|
return std.mem.eql(u8, &lhs.data, &rhs.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(value: Uuid, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||||
|
try std.fmt.format(writer, "{x:0>2}{x:0>2}{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}", .{
|
||||||
|
value.data[0],
|
||||||
|
value.data[1],
|
||||||
|
value.data[2],
|
||||||
|
value.data[3],
|
||||||
|
value.data[4],
|
||||||
|
value.data[5],
|
||||||
|
value.data[6],
|
||||||
|
value.data[7],
|
||||||
|
value.data[8],
|
||||||
|
value.data[9],
|
||||||
|
value.data[10],
|
||||||
|
value.data[11],
|
||||||
|
value.data[12],
|
||||||
|
value.data[13],
|
||||||
|
value.data[14],
|
||||||
|
value.data[15],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ParseError = error{
|
||||||
|
InvalidCharacter,
|
||||||
|
InvalidLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parse(str: []const u8) ParseError!Uuid {
|
||||||
|
if (str.len != StringLen) return error.InvalidLength;
|
||||||
|
|
||||||
|
var uuid: Uuid = undefined;
|
||||||
|
var str_i: usize = 0;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < 16 and str_i < str.len) : ({
|
||||||
|
i += 1;
|
||||||
|
str_i += 2;
|
||||||
|
}) {
|
||||||
|
uuid.data[i] = std.fmt.parseInt(u8, str[str_i .. str_i + 2], 16) catch |err| switch (err) {
|
||||||
|
error.InvalidCharacter => return error.InvalidCharacter,
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (i == 3 or i == 5 or i == 7 or i == 9) {
|
||||||
|
if (str[str_i + 2] != '-') return error.InvalidCharacter;
|
||||||
|
str_i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn randV4(rand: std.rand.Random) Uuid {
|
||||||
|
var ret: Uuid = undefined;
|
||||||
|
rand.bytes(&ret.data);
|
||||||
|
|
||||||
|
// signify that this is a random v4 uuid
|
||||||
|
ret.data[7] = (0b0100_0000) | (ret.data[7] & 0b1111);
|
||||||
|
ret.data[9] = (0b1000_0000) | (ret.data[9] & 0b11_1111);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parse uuid" {
|
||||||
|
try std.testing.expectEqual(
|
||||||
|
Uuid.Nil,
|
||||||
|
try Uuid.parse("00000000-0000-0000-0000-000000000000"),
|
||||||
|
);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(
|
||||||
|
Uuid{
|
||||||
|
.data = @bitCast([16]u8, @as(u128, 0x4ba7b74522ad_1da8_c242_d312_60515ff7)),
|
||||||
|
},
|
||||||
|
try Uuid.parse("f75f5160-12d3-42c2-a81d-ad2245b7a74b"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "format uuid" {
|
||||||
|
try std.testing.expectFmt("00000000-0000-0000-0000-000000000000", "{}", .{Uuid.Nil});
|
||||||
|
|
||||||
|
const uuid = Uuid{
|
||||||
|
.data = @bitCast([16]u8, @as(u128, 0x4ba7b74522ad_1da8_c242_d312_60515ff7)),
|
||||||
|
};
|
||||||
|
try std.testing.expectFmt("f75f5160-12d3-42c2-a81d-ad2245b7a74b", "{}", .{uuid});
|
||||||
|
|
||||||
|
try std.testing.expectError(error.InvalidLength, Uuid.parse("fsdfs"));
|
||||||
|
try std.testing.expectError(error.InvalidCharacter, Uuid.parse("00000000-0000-0000-xxxx-000000000000"));
|
||||||
|
try std.testing.expectError(error.InvalidLength, Uuid.parse("00000000-0000-0000-0000-000000000000fsdfs"));
|
||||||
|
try std.testing.expectError(error.InvalidCharacter, Uuid.parse("00000000-0000x0000-0000-000000000000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "roundtrip random uuid" {
|
||||||
|
const test_seed = 12345;
|
||||||
|
var test_prng = std.rand.DefaultPrng.init(test_seed);
|
||||||
|
const uuid = Uuid.randV4(test_prng.random());
|
||||||
|
|
||||||
|
var buf: [36]u8 = undefined;
|
||||||
|
var fbs = std.io.fixedBufferStream(&buf);
|
||||||
|
try Uuid.format(uuid, "", .{}, fbs.writer());
|
||||||
|
|
||||||
|
const parsed = try Uuid.parse(&buf);
|
||||||
|
|
||||||
|
try std.testing.expectEqual(uuid, parsed);
|
||||||
|
}
|
106
src/util/ciutf8.zig
Normal file
106
src/util/ciutf8.zig
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Hash = std.hash.Wyhash;
|
||||||
|
const View = std.unicode.Utf8View;
|
||||||
|
const toLower = std.ascii.toLower;
|
||||||
|
const isAscii = std.ascii.isASCII;
|
||||||
|
const hash_seed = 1;
|
||||||
|
|
||||||
|
pub fn hash(str: []const u8) u64 {
|
||||||
|
// fallback to regular hash on invalid utf8
|
||||||
|
const view = View.init(str) catch return Hash.hash(hash_seed, str);
|
||||||
|
var iter = view.iterator();
|
||||||
|
|
||||||
|
var h = Hash.init(hash_seed);
|
||||||
|
|
||||||
|
var it = iter.nextCodepointSlice();
|
||||||
|
while (it != null) : (it = iter.nextCodepointSlice()) {
|
||||||
|
if (it.?.len == 1 and isAscii(it.?[0])) {
|
||||||
|
const ch = [1]u8{toLower(it.?[0])};
|
||||||
|
h.update(&ch);
|
||||||
|
} else {
|
||||||
|
h.update(it.?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.final();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eql(a: []const u8, b: []const u8) bool {
|
||||||
|
if (a.len != b.len) return false;
|
||||||
|
|
||||||
|
const va = View.init(a) catch return std.mem.eql(u8, a, b);
|
||||||
|
const vb = View.init(b) catch return false;
|
||||||
|
|
||||||
|
var iter_a = va.iterator();
|
||||||
|
var iter_b = vb.iterator();
|
||||||
|
|
||||||
|
var it_a = iter_a.nextCodepointSlice();
|
||||||
|
var it_b = iter_b.nextCodepointSlice();
|
||||||
|
|
||||||
|
while (it_a != null and it_b != null) : ({
|
||||||
|
it_a = iter_a.nextCodepointSlice();
|
||||||
|
it_b = iter_b.nextCodepointSlice();
|
||||||
|
}) {
|
||||||
|
if (it_a.?.len != it_b.?.len) return false;
|
||||||
|
|
||||||
|
if (it_a.?.len == 1) {
|
||||||
|
if (isAscii(it_a.?[0]) and isAscii(it_b.?[0])) {
|
||||||
|
const ch_a = toLower(it_a.?[0]);
|
||||||
|
const ch_b = toLower(it_b.?[0]);
|
||||||
|
|
||||||
|
if (ch_a != ch_b) return false;
|
||||||
|
} else if (it_a.?[0] != it_b.?[0]) return false;
|
||||||
|
} else if (!std.mem.eql(u8, it_a.?, it_b.?)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return it_a == null and it_b == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "case insensitive eql with utf-8 chars" {
|
||||||
|
const t = std.testing;
|
||||||
|
try t.expectEqual(true, eql("abc 💯 def", "aBc 💯 DEF"));
|
||||||
|
try t.expectEqual(false, eql("xyz 💯 ijk", "aBc 💯 DEF"));
|
||||||
|
try t.expectEqual(false, eql("abc 💯 def", "aBc x DEF"));
|
||||||
|
try t.expectEqual(true, eql("💯", "💯"));
|
||||||
|
try t.expectEqual(false, eql("💯", "a"));
|
||||||
|
try t.expectEqual(false, eql("💯", "💯 continues"));
|
||||||
|
try t.expectEqual(false, eql("💯 fsdfs", "💯"));
|
||||||
|
try t.expectEqual(false, eql("💯", ""));
|
||||||
|
try t.expectEqual(false, eql("", "💯"));
|
||||||
|
|
||||||
|
try t.expectEqual(true, eql("abc x def", "aBc x DEF"));
|
||||||
|
try t.expectEqual(false, eql("xyz x ijk", "aBc x DEF"));
|
||||||
|
try t.expectEqual(true, eql("x", "x"));
|
||||||
|
try t.expectEqual(false, eql("x", "a"));
|
||||||
|
try t.expectEqual(false, eql("x", "x continues"));
|
||||||
|
try t.expectEqual(false, eql("x fsdfs", "x"));
|
||||||
|
try t.expectEqual(false, eql("x", ""));
|
||||||
|
try t.expectEqual(false, eql("", "x"));
|
||||||
|
|
||||||
|
try t.expectEqual(true, eql("", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "case insensitive hash with utf-8 chars" {
|
||||||
|
const t = std.testing;
|
||||||
|
try t.expect(hash("abc 💯 def") == hash("aBc 💯 DEF"));
|
||||||
|
try t.expect(hash("xyz 💯 ijk") != hash("aBc 💯 DEF"));
|
||||||
|
try t.expect(hash("abc 💯 def") != hash("aBc x DEF"));
|
||||||
|
try t.expect(hash("💯") == hash("💯"));
|
||||||
|
try t.expect(hash("💯") != hash("a"));
|
||||||
|
try t.expect(hash("💯") != hash("💯 continues"));
|
||||||
|
try t.expect(hash("💯 fsdfs") != hash("💯"));
|
||||||
|
try t.expect(hash("💯") != hash(""));
|
||||||
|
try t.expect(hash("") != hash("💯"));
|
||||||
|
|
||||||
|
try t.expect(hash("abc x def") == hash("aBc x DEF"));
|
||||||
|
try t.expect(hash("xyz x ijk") != hash("aBc x DEF"));
|
||||||
|
try t.expect(hash("x") == hash("x"));
|
||||||
|
try t.expect(hash("x") != hash("a"));
|
||||||
|
try t.expect(hash("x") != hash("x continues"));
|
||||||
|
try t.expect(hash("x fsdfs") != hash("x"));
|
||||||
|
try t.expect(hash("x") != hash(""));
|
||||||
|
try t.expect(hash("") != hash("x"));
|
||||||
|
|
||||||
|
try t.expect(hash("") == hash(""));
|
||||||
|
}
|
7
src/util/lib.zig
Normal file
7
src/util/lib.zig
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
pub const ciutf8 = @import("./ciutf8.zig");
|
||||||
|
pub const Uuid = @import("./Uuid.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = ciutf8;
|
||||||
|
_ = Uuid;
|
||||||
|
}
|
Loading…
Reference in a new issue