commit e3f0ff1d84255bcd55471f42971d6f2a840ca90e Author: MrBounty Date: Mon Sep 2 11:02:33 2024 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad8bcfb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.zig-cache \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3de059d --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ +# ZipponDB + +Note: Make a stupide mascotte + +# Written in Zig + +Zig is fast, blablabla + +# How it's work + +Meme "That's the neat part..." + +Zippon is a strutural relational potentially in memory written entirely in Zig from stractch. + +You build a binary according to your schema, you can just run it with some arguments and it will create and manage a folder 'zipponDB_DATA'. +Then you do what you want with it, including: +- Run it with your app as a file and folder +- Create a Docker and open some port +- Create a Docker with a small API +- Other stuffs, Im sure some will find something nice + +Note that you can have multiple binary that run together. Each binary have a unique id that is use to segregate binary inside the folder 'zipponDB_DATA' + +# Benchmark + +# Create a schema + +Zippon use struct as way of saving data. A struct is a way of storing multiple data of different type. +Very similar to a row in a table, columns being datatype and a row a single struct. + +The schema is directly INSIDE the binary, so each binary are per schema ! This is for effenciency, idk to be honest, I guess ? lol + +# Migration + +For now you can't migrate the data of one binary to another, so you will need to different binary. + +# Zippon language + +Ok so I went crazy on that, on have it how language. It is stupide and I love it. I wanted to do like EdgeDB but no, too simple. +Anyway, I tried to do something different, to do something different, idk, you're the jduge of it. + +``` +GRAB User { name = 'Adrien' } +Get all user named Adrien + +GRAB User [1; email] { } +Get one email + +GRAB User {} | ASCENDING name | +Get all users ordered by name + +GRAB User [name] { age > 10 AND name != 'Adrien' } | DECENDING age | +Get just the name of all users that are 10 years old or more and not named Adrien ordered by age + +GRAB User { bestfriend = { name = 'Adrien' } } +GRAB User { bestfriend = User{ name = 'Adrien' } } // Same +Get all user that have a best friend named Adrien + +GRAB User [10] { IN User [1] { age > 10 } | ASC name |.friends } +Get 10 users that are friend with the first user older than 10 years old in ascending name order + +GRAB Message [100; comments [ date ] ] { .writter = { name = 'Adrien' }.bestfriend } +Get the date of 100 comments from the best friend of the writter named Adrien + +GRAB User { IN Message { date > '12-01-2014' }.writter } +Get all users that sended a message after the 12 january 2014 + +GRAB User { !IN Comment { }.writter } +Get all user that didn't wrote a comment + +GRAB User { IN User { name = 'Adrien' }.friends } +Get all user that are friends with an Adrien + +UPDATE User [1] { name = 'Adrien' } => ( email = 'new@email.com' ) + +REMOVE User { id = '000-000' } + +ADD User ( name = 'Adrien', email = 'email', age = 40 } +``` + +- {} Are filters +- [] Are how much; what data +- () Are new or updated data (Not already savec) +- || Are additional options +- Data are in struct format and can have link + + +# How it's really work + +NOTE: Do this in a separe file + +## Tokenizer + +The tokenizer of the language is +# ZipponDB diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..2873be2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +# UUID +[ ] Create new random UUID +[ ] Find one UUID in an array + +Possible way to look for data diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..516ce9a --- /dev/null +++ b/build.zig @@ -0,0 +1,66 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "zippon2", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..bb394a6 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,72 @@ +.{ + // This is the default name used by packages depending on this one. For + // example, when a user runs `zig fetch --save `, this field is used + // as the key in the `dependencies` table. Although the user can choose a + // different name, most users will stick with this provided value. + // + // It is redundant to include "zig" in this name because it is already + // within the Zig package namespace. + .name = "zippon2", + + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save ` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. Only files listed here will remain on disk + // when using the zig package manager. As a rule of thumb, one should list + // files required for compilation plus any license(s). + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + // For example... + //"LICENSE", + //"README.md", + }, +} diff --git a/src/dtypes.zig b/src/dtypes.zig new file mode 100644 index 0000000..53c1959 --- /dev/null +++ b/src/dtypes.zig @@ -0,0 +1,42 @@ +const std = @import("std"); +const UUID = @import("uuid.zig").UUID; + +pub const Types = union { + user: *User, + message: *Message, +}; + +pub const User = struct { + id: UUID, + name: []const u8, + email: []const u8, + messages: std.ArrayList(*Message), + + pub fn new(allocator: std.mem.Allocator, name: []const u8, email: []const u8) !*User { + const user = try allocator.create(User); + user.* = .{ + .id = UUID.init(), + .name = name, + .email = email, + .messages = std.ArrayList(*Message).init(allocator), + }; + return user; + } +}; + +pub const Message = struct { + id: UUID, + content: []const u8, + user: *User, + + pub fn new(allocator: std.mem.Allocator, content: []const u8, user: *User) !*Message { + const message = try allocator.create(Message); + message.* = .{ + .id = UUID.init(), + .content = content, + .user = user, + }; + try user.*.messages.append(message); + return message; + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..633e30c --- /dev/null +++ b/src/main.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const UUID = @import("uuid.zig").UUID; +const dtypes = @import("dtypes.zig"); +const Tokenizer = @import("tokenizer.zig").Tokenizer; +const Token = @import("tokenizer.zig").Token; +const Allocator = std.mem.Allocator; +const print = std.debug.print; + +pub const Error = error{UUIDNotFound}; + +const Commands = enum { run, describe, help, unknow, @"run describe help unknow" }; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + // Init the map storage string map that track all array of struct + var storage = std.StringHashMap(*std.ArrayList(dtypes.Types)).init(allocator); + defer storage.deinit(); + + // Create all array and put them in the main map + var userArray = std.ArrayList(dtypes.Types).init(allocator); + try storage.put("User", &userArray); + + var postArray = std.ArrayList(dtypes.Types).init(allocator); + try storage.put("Post", &postArray); + + var commentArray = std.ArrayList(dtypes.Types).init(allocator); + try storage.put("Comment", &commentArray); + + // Add a new user + const newUser = try dtypes.User.new(allocator, "Adrien", "adrien@gmail.com"); + try storage.get("User").?.append(dtypes.Types{ .user = newUser }); + + std.debug.print("{s}\n", .{storage.get("User").?.items[0].user.email}); + + // Lets get arguments and what the user want to do + var argsIterator = try std.process.ArgIterator.initWithAllocator(allocator); + defer argsIterator.deinit(); + + // Skip executable + _ = argsIterator.next(); + + if (argsIterator.next()) |commandStr| { + const command = std.meta.stringToEnum(Commands, commandStr) orelse Commands.unknow; + switch (command) { + .run => { + const query = argsIterator.next(); + var tokenizer = Tokenizer.init(query.?); + var token = tokenizer.next(); + while (token.tag != Token.Tag.eof) { + std.debug.print("{any}\n", .{token}); + token = tokenizer.next(); + } + }, + .help => { + std.debug.print("Welcome to ZipponDB!.", .{}); + }, + .describe => { + std.debug.print("Here the current schema:\nUser (\n\tname: str,\n\temail:str,\n\tfriend:User\n)\n", .{}); + }, + .unknow => { + std.debug.print("Unknow command, available are: run, describe, help.\n", .{}); + }, + else => {}, + } + } else { + std.debug.print("No args found. Available are: run, help.\n", .{}); + } +} + +fn getById(array: anytype, id: UUID) !*dtypes.User { + for (array.items) |data| { + if (data.id.compare(id)) { + return data; + } + } + return error.UUIDNotFound; +} + +test "getById" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + var users = std.ArrayList(*dtypes.User).init(allocator); + try users.append(try dtypes.User.new(allocator, "Adrien", "adrien@gmail.com")); + + const adrien = try getById(users, users.items[0].id); + + try std.testing.expect(UUID.compare(users.items[0].id, adrien.id)); +} diff --git a/src/tokenizer.zig b/src/tokenizer.zig new file mode 100644 index 0000000..b03beba --- /dev/null +++ b/src/tokenizer.zig @@ -0,0 +1,383 @@ +// From https://github.com/ziglang/zig/blob/master/lib/std/zig/tokenizer.zig +const std = @import("std"); + +pub const Token = struct { + tag: Tag, + loc: Loc, + + pub const Loc = struct { + start: usize, + end: usize, + }; + + pub const keywords = std.StaticStringMap(Tag).initComptime(.{ + .{ "GRAB", .keyword_grab }, + .{ "UPDATE", .keyword_update }, + .{ "DELETE", .keyword_delete }, + .{ "ADD", .keyword_add }, + .{ "IN", .keyword_in }, + }); + + pub fn getKeyword(bytes: []const u8) ?Tag { + return keywords.get(bytes); + } + + pub const Tag = enum { + eof, + invalid, + + keyword_grab, + keyword_update, + keyword_delete, + keyword_add, + keyword_in, + + string_literal, + number_literal, + identifier, + equal, + bang, + pipe, + l_paren, + r_paren, + l_bracket, + r_bracket, + l_brace, + r_brace, + semicolon, + comma, + angle_bracket_left, + angle_bracket_right, + angle_bracket_left_equal, + angle_bracket_right_equal, + equal_angle_bracket_right, + period, + bang_equal, + }; + + pub fn lexeme(tag: Tag) ?[]const u8 { + return switch (tag) { + .invalid, + .identifier, + .string_literal, + .number_literal, + => null, + + .bang => "!", + .pipe => "|", + }; + } +}; + +pub const Tokenizer = struct { + buffer: [:0]const u8, + index: usize, + + /// For debugging purposes. + pub fn dump(self: *Tokenizer, token: *const Token) void { + std.debug.print("{s} \"{s}\"\n", .{ @tagName(token.tag), self.buffer[token.loc.start..token.loc.end] }); + } + + pub fn init(buffer: [:0]const u8) Tokenizer { + // Skip the UTF-8 BOM if present. + return .{ + .buffer = buffer, + .index = if (std.mem.startsWith(u8, buffer, "\xEF\xBB\xBF")) 3 else 0, + }; + } + + const State = enum { + start, + invalid, + string_literal, + identifier, + equal, + bang, + angle_bracket_left, + angle_bracket_right, + string_literal_backslash, + int_exponent, + int_period, + float, + float_exponent, + int, + }; + + pub fn next(self: *Tokenizer) Token { + var state: State = .start; + var result: Token = .{ + .tag = undefined, + .loc = .{ + .start = self.index, + .end = undefined, + }, + }; + while (true) : (self.index += 1) { + const c = self.buffer[self.index]; + switch (state) { + .start => switch (c) { + 0 => { + if (self.index == self.buffer.len) return .{ + .tag = .eof, + .loc = .{ + .start = self.index, + .end = self.index, + }, + }; + state = .invalid; + }, + ' ', '\n', '\t', '\r' => { + result.loc.start = self.index + 1; + }, + '\'' => { + state = .string_literal; + result.tag = .string_literal; + }, + 'a'...'z', 'A'...'Z', '_' => { + state = .identifier; + result.tag = .identifier; + }, + '=' => { + state = .equal; + }, + '!' => { + state = .bang; + }, + '|' => { + result.tag = .pipe; + self.index += 1; + break; + }, + '(' => { + result.tag = .l_paren; + self.index += 1; + break; + }, + ')' => { + result.tag = .r_paren; + self.index += 1; + break; + }, + '[' => { + result.tag = .l_bracket; + self.index += 1; + break; + }, + ']' => { + result.tag = .r_bracket; + self.index += 1; + break; + }, + ';' => { + result.tag = .semicolon; + self.index += 1; + break; + }, + ',' => { + result.tag = .comma; + self.index += 1; + break; + }, + '<' => { + state = .angle_bracket_left; + }, + '>' => { + state = .angle_bracket_right; + }, + '{' => { + result.tag = .l_brace; + self.index += 1; + break; + }, + '}' => { + result.tag = .r_brace; + self.index += 1; + break; + }, + '.' => { + result.tag = .period; + self.index += 1; + break; + }, + '0'...'9' => { + state = .int; + result.tag = .number_literal; + }, + else => { + state = .invalid; + }, + }, + + .invalid => { + // TODO make a better invalid handler + @panic("Unknow char!!!"); + }, + + .identifier => switch (c) { + 'a'...'z', 'A'...'Z', '_', '0'...'9' => continue, + else => { + if (Token.getKeyword(self.buffer[result.loc.start..self.index])) |tag| { + result.tag = tag; + } + break; + }, + }, + + .string_literal => switch (c) { + 0 => { + if (self.index != self.buffer.len) { + state = .invalid; + continue; + } + result.tag = .invalid; + break; + }, + '\n' => { + result.tag = .invalid; + break; + }, + '\\' => { + state = .string_literal_backslash; + }, + '\'' => { + self.index += 1; + break; + }, + 0x01...0x09, 0x0b...0x1f, 0x7f => { + state = .invalid; + }, + else => continue, + }, + + .string_literal_backslash => switch (c) { + 0, '\n' => { + result.tag = .invalid; + break; + }, + else => { + state = .string_literal; + }, + }, + + .bang => switch (c) { + '=' => { + result.tag = .bang_equal; + self.index += 1; + break; + }, + //TODO Add the !IN + else => { + result.tag = .bang; + break; + }, + }, + + .equal => switch (c) { + '>' => { + result.tag = .equal_angle_bracket_right; + self.index += 1; + break; + }, + else => { + result.tag = .equal; + break; + }, + }, + + .angle_bracket_left => switch (c) { + '=' => { + result.tag = .angle_bracket_left_equal; + self.index += 1; + break; + }, + else => { + result.tag = .angle_bracket_left; + break; + }, + }, + + .angle_bracket_right => switch (c) { + '=' => { + result.tag = .angle_bracket_right_equal; + self.index += 1; + break; + }, + else => { + result.tag = .angle_bracket_right; + break; + }, + }, + + .int => switch (c) { + '.' => state = .int_period, + '_', 'a'...'d', 'f'...'o', 'q'...'z', 'A'...'D', 'F'...'O', 'Q'...'Z', '0'...'9' => continue, + 'e', 'E', 'p', 'P' => state = .int_exponent, + else => break, + }, + .int_exponent => switch (c) { + '-', '+' => { + state = .float; + }, + else => { + self.index -= 1; + state = .int; + }, + }, + .int_period => switch (c) { + '_', 'a'...'d', 'f'...'o', 'q'...'z', 'A'...'D', 'F'...'O', 'Q'...'Z', '0'...'9' => { + state = .float; + }, + 'e', 'E', 'p', 'P' => state = .float_exponent, + else => { + self.index -= 1; + break; + }, + }, + .float => switch (c) { + '_', 'a'...'d', 'f'...'o', 'q'...'z', 'A'...'D', 'F'...'O', 'Q'...'Z', '0'...'9' => continue, + 'e', 'E', 'p', 'P' => state = .float_exponent, + else => break, + }, + .float_exponent => switch (c) { + '-', '+' => state = .float, + else => { + self.index -= 1; + state = .float; + }, + }, + } + } + + result.loc.end = self.index; + return result; + } +}; + +test "keywords" { + try testTokenize("GRAB UPDATE ADD DELETE IN", &.{ .keyword_grab, .keyword_update, .keyword_add, .keyword_delete, .keyword_in }); + std.debug.print("Keywords OK\n", .{}); +} + +test "basic query" { + try testTokenize("GRAB User {}", &.{ .keyword_grab, .identifier, .l_brace, .r_brace }); + try testTokenize("GRAB User { name = 'Adrien'}", &.{ .keyword_grab, .identifier, .l_brace, .identifier, .equal, .string_literal, .r_brace }); + try testTokenize("GRAB User [1; name] {}", &.{ .keyword_grab, .identifier, .l_bracket, .number_literal, .semicolon, .identifier, .r_bracket, .l_brace, .r_brace }); + try testTokenize("GRAB User{}|ASCENDING name|", &.{ .keyword_grab, .identifier, .l_brace, .r_brace, .pipe, .identifier, .identifier, .pipe }); + try testTokenize("DELETE User[1]{name='Adrien'}|ASCENDING name, age|", &.{ .keyword_delete, .identifier, .l_bracket, .number_literal, .r_bracket, .l_brace, .identifier, .equal, .string_literal, .r_brace, .pipe, .identifier, .identifier, .comma, .identifier, .pipe }); + std.debug.print("Basic query OK\n", .{}); +} + +fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void { + var tokenizer = Tokenizer.init(source); + for (expected_token_tags) |expected_token_tag| { + const token = tokenizer.next(); + try std.testing.expectEqual(expected_token_tag, token.tag); + } + // Last token should always be eof, even when the last token was invalid, + // in which case the tokenizer is in an invalid state, which can only be + // recovered by opinionated means outside the scope of this implementation. + const last_token = tokenizer.next(); + try std.testing.expectEqual(Token.Tag.eof, last_token.tag); + try std.testing.expectEqual(source.len, last_token.loc.start); + try std.testing.expectEqual(source.len, last_token.loc.end); +} diff --git a/src/uuid.zig b/src/uuid.zig new file mode 100644 index 0000000..c381eb0 --- /dev/null +++ b/src/uuid.zig @@ -0,0 +1,180 @@ +// 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); + } + + 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; + } + + // 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 }; + +// Convenience function to return a new v4 UUID. +pub fn newV4() UUID { + return UUID.init(); +} + +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); + + std.debug.print("\nUUID {s} \n", .{uuid1}); + std.debug.print("\nFirst call to_string {s} \n", .{string1}); + std.debug.print("Second call to_string {s} \n", .{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)); +} diff --git a/zig-out/bin/zippon2 b/zig-out/bin/zippon2 new file mode 100755 index 0000000..5ad8178 Binary files /dev/null and b/zig-out/bin/zippon2 differ