From 7b25c31918358e822d8c8c8ff1f56bb1bbcbf024 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Mar 2024 14:55:51 -0500 Subject: [PATCH 01/11] zig init --- .gitignore | 5 +++ build.zig | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ build.zig.zon | 62 +++++++++++++++++++++++++++++++++++ src/main.zig | 24 ++++++++++++++ src/root.zig | 10 ++++++ 5 files changed, 192 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/main.zig create mode 100644 src/root.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c1baea --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +zig-out/ +zig-cache/ + +/src/vk.zig diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..b63417e --- /dev/null +++ b/build.zig @@ -0,0 +1,91 @@ +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 lib = b.addStaticLibrary(.{ + .name = "learnzig", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/root.zig" }, + .target = target, + .optimize = optimize, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); + + const exe = b.addExecutable(.{ + .name = "learnzig", + .root_source_file = .{ .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); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/root.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + + const exe_unit_tests = b.addTest(.{ + .root_source_file = .{ .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_lib_unit_tests.step); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..8814bdd --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,62 @@ +.{ + .name = "learnzig", + // 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", + //}, + }, + + // 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. + // 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 = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..c8a3f67 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +pub fn main() !void { + // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + + // stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Run `zig build test` to run the tests.\n", .{}); + + try bw.flush(); // don't forget to flush! +} + +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..ecfeade --- /dev/null +++ b/src/root.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const testing = std.testing; + +export fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try testing.expect(add(3, 7) == 10); +} From 36f8b3d65b202630bc90bca03233b213ceff96c9 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Mar 2024 16:18:06 -0500 Subject: [PATCH 02/11] random testing --- build.zig | 61 ++---------------------------------- build.zig.zon | 34 +++----------------- src/main.zig | 86 ++++++++++++++++++++++++++++++++++++++++++--------- src/root.zig | 10 ------ 4 files changed, 78 insertions(+), 113 deletions(-) delete mode 100644 src/root.zig diff --git a/build.zig b/build.zig index b63417e..e7323ac 100644 --- a/build.zig +++ b/build.zig @@ -1,33 +1,11 @@ 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 opts = .{ .target = target, .optimize = optimize }; - const lib = b.addStaticLibrary(.{ - .name = "learnzig", - // In this case the main source file is merely a path, however, in more - // complicated build scripts, this could be a generated file. - .root_source_file = .{ .path = "src/root.zig" }, - .target = target, - .optimize = optimize, - }); - - // This declares intent for the library to be installed into the standard - // location when the user invokes the "install" step (the default step when - // running `zig build`). - b.installArtifact(lib); + const json = b.dependency("json", opts).module("json"); const exe = b.addExecutable(.{ .name = "learnzig", @@ -35,57 +13,24 @@ pub fn build(b: *std.Build) void { .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`). + exe.root_module.addImport("json", json); 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); - // Creates a step for unit testing. This only builds the test executable - // but does not run it. - const lib_unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/root.zig" }, - .target = target, - .optimize = optimize, - }); - - const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - const exe_unit_tests = b.addTest(.{ .root_source_file = .{ .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_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 8814bdd..33785a7 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,37 +15,11 @@ // 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", - //}, + .json = .{ + .url = "git+https://github.com/getty-zig/json.git#main", + .hash = "1220a6a7706fa609ce63e2bf61812515f6ab45273397ca591f925a32e7e507cb7cec", + }, }, - - // 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. - // 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 = .{ // This makes *all* files, recursively, included in this package. It is generally // better to explicitly list the files and directories instead, to insure that diff --git a/src/main.zig b/src/main.zig index c8a3f67..6dbde9b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,24 +1,80 @@ const std = @import("std"); +const json = @import("json"); + +fn concat(ally: std.mem.Allocator, u: []const u8, v: []const u8) ![]const u8 { + const res = try ally.alloc(u8, u.len + v.len); + @memcpy(res[0..u.len], u); + @memcpy(res[u.len .. u.len + v.len], v); + return res; +} + +const Shape = union(enum) { + rect: struct { w: f32, h: f32 }, + square: struct { s: f32 }, + circle: struct { r: f32 }, +}; + +fn show(shape: Shape) void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const all = gpa.allocator(); + const text = json.toSlice(all, shape) catch unreachable; + defer all.free(text); + std.debug.print("shape: {s}\n", .{text}); + // switch (shape) { + // Shape.rect => |r| std.debug.print("rect: {} x {}\n", .{ r.w, r.h }), + // Shape.square => |s| std.debug.print("square: {}\n", .{s.s}), + // Shape.circle => |c| std.debug.print("circle: {}\n", .{c.r}), + // } +} + +const Point = struct { + x: i32, + y: i32, +}; pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + var big_buf: [1 << 20]u8 = undefined; // 1 MB + var fixed = std.heap.FixedBufferAllocator.init(&big_buf); + const ally = fixed.allocator(); - // stdout is for the actual output of your application, for example if you - // are implementing gzip, then only the compressed bytes should be sent to - // stdout, not any debugging messages. - const stdout_file = std.io.getStdOut().writer(); - var bw = std.io.bufferedWriter(stdout_file); - const stdout = bw.writer(); + var s: Shape = Shape{ .circle = .{ .r = 99 } }; + show(s); + s = Shape{ .rect = .{ .w = 1, .h = 2 } }; + show(s); - try stdout.print("Run `zig build test` to run the tests.\n", .{}); + const val = Point{ .x = 1, .y = 2 }; + const ser = try json.toSlice(ally, val); + defer ally.free(ser); - try bw.flush(); // don't forget to flush! + std.debug.print("{s}\n", .{ser}); + + const deser = try json.fromSlice(ally, Point, ser); + std.debug.print("{any}\n", .{deser}); + + var hash_map = std.StringArrayHashMap([]const u8).init(ally); + defer hash_map.deinit(); + + try hash_map.put("foo", "bar"); + try hash_map.put("fizz", "buzz"); + try hash_map.put("qux", "quxx"); + + const x = try concat(ally, hash_map.get("fizz").?, hash_map.get("foo").?); + defer ally.free(x); + + std.debug.print("x={s}\n", .{x}); + + var iter = hash_map.iterator(); + while (iter.next()) |it| { + std.debug.print("{s} {s}\n", .{ it.key_ptr.*, it.value_ptr.* }); + } } -test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); +test concat { + const x = "hello"; + const y = "world"; + + const z = try concat(std.testing.allocator, x, y); + defer std.testing.allocator.free(z); + + try std.testing.expectEqualStrings(z, "helloworld"); } diff --git a/src/root.zig b/src/root.zig deleted file mode 100644 index ecfeade..0000000 --- a/src/root.zig +++ /dev/null @@ -1,10 +0,0 @@ -const std = @import("std"); -const testing = std.testing; - -export fn add(a: i32, b: i32) i32 { - return a + b; -} - -test "basic add functionality" { - try testing.expect(add(3, 7) == 10); -} From 09e08c3c4ce8b164c10624c9dc6b367c429b5fb0 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Mar 2024 16:18:19 -0500 Subject: [PATCH 03/11] Revert "random testing" This reverts commit 36f8b3d65b202630bc90bca03233b213ceff96c9. --- build.zig | 63 ++++++++++++++++++++++++++++++++++--- build.zig.zon | 34 +++++++++++++++++--- src/main.zig | 86 +++++++++------------------------------------------ src/root.zig | 10 ++++++ 4 files changed, 114 insertions(+), 79 deletions(-) create mode 100644 src/root.zig diff --git a/build.zig b/build.zig index e7323ac..b63417e 100644 --- a/build.zig +++ b/build.zig @@ -1,11 +1,33 @@ 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(.{}); - const optimize = b.standardOptimizeOption(.{}); - const opts = .{ .target = target, .optimize = optimize }; - const json = b.dependency("json", opts).module("json"); + // 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 lib = b.addStaticLibrary(.{ + .name = "learnzig", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/root.zig" }, + .target = target, + .optimize = optimize, + }); + + // This declares intent for the library to be installed into the standard + // location when the user invokes the "install" step (the default step when + // running `zig build`). + b.installArtifact(lib); const exe = b.addExecutable(.{ .name = "learnzig", @@ -13,24 +35,57 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - exe.root_module.addImport("json", json); + + // 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); + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const lib_unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/root.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + const exe_unit_tests = b.addTest(.{ .root_source_file = .{ .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_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 33785a7..8814bdd 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,11 +15,37 @@ // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ - .json = .{ - .url = "git+https://github.com/getty-zig/json.git#main", - .hash = "1220a6a7706fa609ce63e2bf61812515f6ab45273397ca591f925a32e7e507cb7cec", - }, + // 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", + //}, }, + + // 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. + // 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 = .{ // This makes *all* files, recursively, included in this package. It is generally // better to explicitly list the files and directories instead, to insure that diff --git a/src/main.zig b/src/main.zig index 6dbde9b..c8a3f67 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,80 +1,24 @@ const std = @import("std"); -const json = @import("json"); - -fn concat(ally: std.mem.Allocator, u: []const u8, v: []const u8) ![]const u8 { - const res = try ally.alloc(u8, u.len + v.len); - @memcpy(res[0..u.len], u); - @memcpy(res[u.len .. u.len + v.len], v); - return res; -} - -const Shape = union(enum) { - rect: struct { w: f32, h: f32 }, - square: struct { s: f32 }, - circle: struct { r: f32 }, -}; - -fn show(shape: Shape) void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const all = gpa.allocator(); - const text = json.toSlice(all, shape) catch unreachable; - defer all.free(text); - std.debug.print("shape: {s}\n", .{text}); - // switch (shape) { - // Shape.rect => |r| std.debug.print("rect: {} x {}\n", .{ r.w, r.h }), - // Shape.square => |s| std.debug.print("square: {}\n", .{s.s}), - // Shape.circle => |c| std.debug.print("circle: {}\n", .{c.r}), - // } -} - -const Point = struct { - x: i32, - y: i32, -}; pub fn main() !void { - var big_buf: [1 << 20]u8 = undefined; // 1 MB - var fixed = std.heap.FixedBufferAllocator.init(&big_buf); - const ally = fixed.allocator(); + // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) + std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); - var s: Shape = Shape{ .circle = .{ .r = 99 } }; - show(s); - s = Shape{ .rect = .{ .w = 1, .h = 2 } }; - show(s); + // stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); - const val = Point{ .x = 1, .y = 2 }; - const ser = try json.toSlice(ally, val); - defer ally.free(ser); + try stdout.print("Run `zig build test` to run the tests.\n", .{}); - std.debug.print("{s}\n", .{ser}); - - const deser = try json.fromSlice(ally, Point, ser); - std.debug.print("{any}\n", .{deser}); - - var hash_map = std.StringArrayHashMap([]const u8).init(ally); - defer hash_map.deinit(); - - try hash_map.put("foo", "bar"); - try hash_map.put("fizz", "buzz"); - try hash_map.put("qux", "quxx"); - - const x = try concat(ally, hash_map.get("fizz").?, hash_map.get("foo").?); - defer ally.free(x); - - std.debug.print("x={s}\n", .{x}); - - var iter = hash_map.iterator(); - while (iter.next()) |it| { - std.debug.print("{s} {s}\n", .{ it.key_ptr.*, it.value_ptr.* }); - } + try bw.flush(); // don't forget to flush! } -test concat { - const x = "hello"; - const y = "world"; - - const z = try concat(std.testing.allocator, x, y); - defer std.testing.allocator.free(z); - - try std.testing.expectEqualStrings(z, "helloworld"); +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); } diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..ecfeade --- /dev/null +++ b/src/root.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const testing = std.testing; + +export fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try testing.expect(add(3, 7) == 10); +} From 7cd5f61256ecc32fdc48dcd09abc89cf2126a355 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Mar 2024 17:24:44 -0500 Subject: [PATCH 04/11] WIP: dh / aes testing --- src/main.zig | 120 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 110 insertions(+), 10 deletions(-) diff --git a/src/main.zig b/src/main.zig index c8a3f67..79969c6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,19 +1,119 @@ const std = @import("std"); +const DH = std.crypto.dh.X25519; +const AES = std.crypto.aead.aes_gcm.Aes256Gcm; +const SHA = std.crypto.hash.sha2.Sha256; + +const Encryptor = struct { + const Self = @This(); + + kp: DH.KeyPair, + + pub fn init() !Self { + const self = Self{}; + + var seed: [DH.seed_length]u8 = undefined; + std.crypto.random.bytes(&seed); + self.kp = try DH.KeyPair.create(seed); + @memset(seed, 0); + + return self; + } +}; + +const Peer = struct { + const Self = @This(); + + public_key: [DH.public_length]u8, + shared_key: [AES.key_length]u8, + + pub fn init(me: DH.KeyPair, peer_key: [DH.public_length]u8) Self { + const self = Self{ .public_key = peer_key }; + + const secret = try DH.scalarmult(me.secret_key, peer_key); + SHA.hash(&secret, self.shared_key, .{}); + + return self; + } + + pub fn encrypt(ally: std.mem.Allocator, msg: []const u8) !Packet { + var pkt: Packet = undefined; + std.crypto.random.bytes(&pkt.nonce); + } +}; + +const Packet = struct { + ally: std.mem.Allocator, + nonce: [AES.tag_length]u8, + + cipher_text: []u8, +}; + pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + const me = try Encryptor.init(); + const him = try Encryptor.init(); - // stdout is for the actual output of your application, for example if you - // are implementing gzip, then only the compressed bytes should be sent to - // stdout, not any debugging messages. - const stdout_file = std.io.getStdOut().writer(); - var bw = std.io.bufferedWriter(stdout_file); - const stdout = bw.writer(); + const my_peer = Peer.init(me, him.kp.public_key); + const his_peer = Peer.init(him, me.kp.public_key); - try stdout.print("Run `zig build test` to run the tests.\n", .{}); + var seed: [dh.X25519.seed_length]u8 = undefined; + std.crypto.random.bytes(&seed); + const kp1 = try dh.X25519.KeyPair.create(seed); + std.crypto.random.bytes(&seed); + const kp2 = try dh.X25519.KeyPair.create(seed); - try bw.flush(); // don't forget to flush! + std.debug.print("{any}\n{any}\n\n", .{ kp1.public_key, kp1.secret_key }); + std.debug.print("{any}\n{any}\n\n", .{ kp2.public_key, kp2.secret_key }); + + const sec_1 = try dh.X25519.scalarmult(kp1.secret_key, kp2.public_key); + const sec_2 = try dh.X25519.scalarmult(kp2.secret_key, kp1.public_key); + + var key: [aead.aes_gcm.Aes256Gcm.key_length]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(&sec_1, &key, .{}); + + std.debug.print("{any}\n", .{sec_1}); + std.debug.print("{any}\n", .{sec_2}); + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const ally = gpa.allocator(); + + const m: []const u8 = "hello world!"; + const ad: []const u8 = "foo"; + + // see https://crypto.stackexchange.com/a/5818 + // the nonce does not need to be random, it can be a counter + // it can be sent in clear, or each end may deduce it + // but it ___MUST___ be unique and unrelated to the key + var iv: [aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; + std.crypto.random.bytes(&iv); + + const c: []u8 = try ally.alloc(u8, m.len); + defer ally.free(c); + + @memset(c, 0); + var tag: [aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; + + std.debug.print("c: {any}\n", .{c}); + aead.aes_gcm.Aes256Gcm.encrypt(c, &tag, m, ad, iv, key); + std.debug.print("{any} {any} {any}\n", .{ c, tag, iv }); + + // aead.aes_gcm.Aes256Gcm.encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) + + // var seed_a = [sign.Ed25519.KeyPair.seed_length]u8; + // std.crypto.random.bytes(&seed_a); + // const pair_a = try sign.Ed25519.KeyPair.create(seed_a); + // var pubkey_a = dh.X25519.publicKeyFromEd25519(pair_a.public_key); + // + // var seed_b = [sign.Ed25519.KeyPair.seed_length]u8; + // std.crypto.random.bytes(&seed_b); + // const pair_b = try sign.Ed25519.KeyPair.create(seed_b); + // var pubkey_b = try dh.X25519.publicKeyFromEd25519(pair_b.public_key); + + // var buf: [64]u8 = undefined; + // @memset(&buf, 0); + // std.debug.print("{any}\n", .{buf}); + // std.crypto.random.bytes(&buf); + // std.debug.print("{any}\n", .{buf}); } test "simple test" { From 12ab3391e81fe6f904ecef76ad4514fbecbd89b0 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Mar 2024 17:28:29 -0500 Subject: [PATCH 05/11] random testing --- src/main.zig | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main.zig b/src/main.zig index 79969c6..f943360 100644 --- a/src/main.zig +++ b/src/main.zig @@ -36,15 +36,25 @@ const Peer = struct { return self; } - pub fn encrypt(ally: std.mem.Allocator, msg: []const u8) !Packet { + /// Caller must free result + pub fn encrypt(self: Self, ally: std.mem.Allocator, msg: []const u8) !Packet { var pkt: Packet = undefined; std.crypto.random.bytes(&pkt.nonce); + pkt.cipher_text = try ally.alloc(u8, msg.len); + AES.encrypt(pkt.cipher_text, &pkt.tag, msg, "", pkt.nonce, self.shared_key); + return pkt; + } + + /// Caller must free result + pub fn decrypt(self: Self, ally: std.mem.Allocator, msg: []const u8) ![]const u8 { + } }; const Packet = struct { ally: std.mem.Allocator, - nonce: [AES.tag_length]u8, + nonce: [AES.nonce_length]u8, + tag: [AES.tag_length]u8, cipher_text: []u8, }; From 4fc1436431013af44b5926b84616ddfeac5febde Mon Sep 17 00:00:00 2001 From: David Allemang Date: Mon, 4 Mar 2024 20:45:42 -0500 Subject: [PATCH 06/11] working dh aes flow --- src/main.zig | 157 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 113 insertions(+), 44 deletions(-) diff --git a/src/main.zig b/src/main.zig index f943360..97aa33b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,12 +10,12 @@ const Encryptor = struct { kp: DH.KeyPair, pub fn init() !Self { - const self = Self{}; + var self: Self = undefined; var seed: [DH.seed_length]u8 = undefined; std.crypto.random.bytes(&seed); self.kp = try DH.KeyPair.create(seed); - @memset(seed, 0); + @memset(&seed, 0); return self; } @@ -27,11 +27,12 @@ const Peer = struct { public_key: [DH.public_length]u8, shared_key: [AES.key_length]u8, - pub fn init(me: DH.KeyPair, peer_key: [DH.public_length]u8) Self { - const self = Self{ .public_key = peer_key }; + pub fn init(me: DH.KeyPair, peer_key: [DH.public_length]u8) !Self { + var self: Self = undefined; + self.public_key = peer_key; const secret = try DH.scalarmult(me.secret_key, peer_key); - SHA.hash(&secret, self.shared_key, .{}); + SHA.hash(&secret, &self.shared_key, .{}); return self; } @@ -46,8 +47,10 @@ const Peer = struct { } /// Caller must free result - pub fn decrypt(self: Self, ally: std.mem.Allocator, msg: []const u8) ![]const u8 { - + pub fn decrypt(self: Self, ally: std.mem.Allocator, pkt: Packet) ![]const u8 { + const msg: []u8 = try ally.alloc(u8, pkt.cipher_text.len); + try AES.decrypt(msg, pkt.cipher_text, pkt.tag, "", pkt.nonce, self.shared_key); + return msg; } }; @@ -55,57 +58,108 @@ const Packet = struct { ally: std.mem.Allocator, nonce: [AES.nonce_length]u8, tag: [AES.tag_length]u8, - cipher_text: []u8, }; pub fn main() !void { - const me = try Encryptor.init(); - const him = try Encryptor.init(); + const bob = try Encryptor.init(); + const alice = try Encryptor.init(); - const my_peer = Peer.init(me, him.kp.public_key); - const his_peer = Peer.init(him, me.kp.public_key); + std.debug.print( + "Bob sends {s}\n", + .{std.fmt.bytesToHex(bob.kp.public_key, .lower)}, + ); + std.debug.print( + "Alice sends {s}\n", + .{std.fmt.bytesToHex(alice.kp.public_key, .lower)}, + ); - var seed: [dh.X25519.seed_length]u8 = undefined; - std.crypto.random.bytes(&seed); - const kp1 = try dh.X25519.KeyPair.create(seed); - std.crypto.random.bytes(&seed); - const kp2 = try dh.X25519.KeyPair.create(seed); + const bob_peer = try Peer.init(bob.kp, alice.kp.public_key); + const alice_peer = try Peer.init(alice.kp, bob.kp.public_key); - std.debug.print("{any}\n{any}\n\n", .{ kp1.public_key, kp1.secret_key }); - std.debug.print("{any}\n{any}\n\n", .{ kp2.public_key, kp2.secret_key }); - - const sec_1 = try dh.X25519.scalarmult(kp1.secret_key, kp2.public_key); - const sec_2 = try dh.X25519.scalarmult(kp2.secret_key, kp1.public_key); - - var key: [aead.aes_gcm.Aes256Gcm.key_length]u8 = undefined; - std.crypto.hash.sha2.Sha256.hash(&sec_1, &key, .{}); - - std.debug.print("{any}\n", .{sec_1}); - std.debug.print("{any}\n", .{sec_2}); + std.debug.print( + "Bob deduces {s}\n", + .{std.fmt.bytesToHex(bob_peer.shared_key, .lower)}, + ); + std.debug.print( + "Alice deduces {s}\n", + .{std.fmt.bytesToHex(alice_peer.shared_key, .lower)}, + ); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const ally = gpa.allocator(); + var arena = std.heap.ArenaAllocator.init(gpa.allocator()); + defer arena.deinit(); - const m: []const u8 = "hello world!"; - const ad: []const u8 = "foo"; + const ally = arena.allocator(); - // see https://crypto.stackexchange.com/a/5818 - // the nonce does not need to be random, it can be a counter - // it can be sent in clear, or each end may deduce it - // but it ___MUST___ be unique and unrelated to the key - var iv: [aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; - std.crypto.random.bytes(&iv); + // bob send a message to alice + const msg = "Hello World!"; - const c: []u8 = try ally.alloc(u8, m.len); - defer ally.free(c); + std.debug.print( + "Bob sending message '{s}' ({s})\n", + .{ msg, std.fmt.bytesToHex(msg, .lower) }, + ); - @memset(c, 0); - var tag: [aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; + const pkt = try bob_peer.encrypt(ally, msg); + defer ally.free(pkt.cipher_text); - std.debug.print("c: {any}\n", .{c}); - aead.aes_gcm.Aes256Gcm.encrypt(c, &tag, m, ad, iv, key); - std.debug.print("{any} {any} {any}\n", .{ c, tag, iv }); + std.debug.print( + "Packet: {s} {s} {s}\n", + .{ + try digest(ally, pkt.cipher_text, .lower), + try digest(ally, &pkt.nonce, .lower), + try digest(ally, &pkt.tag, .lower), + }, + ); + std.debug.assert(!std.mem.eql(u8, pkt.cipher_text, msg)); + + const rcv = try alice_peer.decrypt(ally, pkt); + defer ally.free(rcv); + std.debug.print( + "Alice received '{s}' ({s})\n", + .{ rcv, try digest(ally, rcv, .lower) }, + ); + + // var seed: [dh.X25519.seed_length]u8 = undefined; + // std.crypto.random.bytes(&seed); + // const kp1 = try dh.X25519.KeyPair.create(seed); + // std.crypto.random.bytes(&seed); + // const kp2 = try dh.X25519.KeyPair.create(seed); + // + // std.debug.print("{any}\n{any}\n\n", .{ kp1.public_key, kp1.secret_key }); + // std.debug.print("{any}\n{any}\n\n", .{ kp2.public_key, kp2.secret_key }); + // + // const sec_1 = try dh.X25519.scalarmult(kp1.secret_key, kp2.public_key); + // const sec_2 = try dh.X25519.scalarmult(kp2.secret_key, kp1.public_key); + // + // var key: [aead.aes_gcm.Aes256Gcm.key_length]u8 = undefined; + // std.crypto.hash.sha2.Sha256.hash(&sec_1, &key, .{}); + // + // std.debug.print("{any}\n", .{sec_1}); + // std.debug.print("{any}\n", .{sec_2}); + // + // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + // const ally = gpa.allocator(); + // + // const m: []const u8 = "hello world!"; + // const ad: []const u8 = "foo"; + // + // // see https://crypto.stackexchange.com/a/5818 + // // the nonce does not need to be random, it can be a counter + // // it can be sent in clear, or each end may deduce it + // // but it ___MUST___ be unique and unrelated to the key + // var iv: [aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; + // std.crypto.random.bytes(&iv); + // + // const c: []u8 = try ally.alloc(u8, m.len); + // defer ally.free(c); + // + // @memset(c, 0); + // var tag: [aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; + // + // std.debug.print("c: {any}\n", .{c}); + // aead.aes_gcm.Aes256Gcm.encrypt(c, &tag, m, ad, iv, key); + // std.debug.print("{any} {any} {any}\n", .{ c, tag, iv }); // aead.aes_gcm.Aes256Gcm.encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) @@ -132,3 +186,18 @@ test "simple test" { try list.append(42); try std.testing.expectEqual(@as(i32, 42), list.pop()); } + +/// caller takes ownership of result +fn digest(ally: std.mem.Allocator, data: []const u8, case: std.fmt.Case) ![]const u8 { + const charset = switch (case) { + .lower => "0123456789abcdef", + .upper => "0123456789ABCDEF", + }; + + var res = try ally.alloc(u8, data.len * 2); + for (data, 0..) |ch, idx| { + res[idx * 2 + 0] = charset[ch >> 4]; + res[idx * 2 + 1] = charset[ch & 0xf]; + } + return res; +} From 3b79a870956f27496de4e93abd043cabaa059fa2 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 5 Mar 2024 10:44:39 -0500 Subject: [PATCH 07/11] test in-place encrypt decrypt. it works --- src/main.zig | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main.zig b/src/main.zig index 97aa33b..6875ac0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -201,3 +201,31 @@ fn digest(ally: std.mem.Allocator, data: []const u8, case: std.fmt.Case) ![]cons } return res; } + +// it even works in ReleaseFast so I think it's ok. +test "in-place-encrypt" { + const al = std.testing.allocator; + + var key: [AES.key_length]u8 = undefined; + std.crypto.random.bytes(&key); + const iv: [AES.nonce_length]u8 = undefined; + std.crypto.random.bytes(&key); + + var tag: [AES.tag_length]u8 = undefined; + + // 1mb message. very big. should find an error if one exists. + const original_message: []u8 = try al.alloc(u8, 1 << 20); + defer al.free(original_message); + std.crypto.random.bytes(original_message); + + const message = try al.dupe(u8, original_message); + defer al.free(message); + + const ad: []u8 = try al.dupe(u8, "u"); + defer al.free(ad); + + AES.encrypt(message, &tag, message, ad, iv, key); + try std.testing.expect(!std.mem.eql(u8, message, original_message)); + try AES.decrypt(message, message, tag, ad, iv, key); + try std.testing.expect(std.mem.eql(u8, message, original_message)); +} From 24f32f572599b9f3a6cf5f066c18eaf0d2e311a0 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 5 Mar 2024 12:48:16 -0500 Subject: [PATCH 08/11] add enet --- build.zig | 70 ++++++++++++++++----------------------------------- build.zig.zon | 55 +++------------------------------------- src/enet.zig | 3 +++ src/main.zig | 10 +++++--- src/root.zig | 10 -------- 5 files changed, 35 insertions(+), 113 deletions(-) create mode 100644 src/enet.zig delete mode 100644 src/root.zig diff --git a/build.zig b/build.zig index b63417e..f4488e2 100644 --- a/build.zig +++ b/build.zig @@ -1,33 +1,32 @@ 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 lib = b.addStaticLibrary(.{ - .name = "learnzig", - // In this case the main source file is merely a path, however, in more - // complicated build scripts, this could be a generated file. - .root_source_file = .{ .path = "src/root.zig" }, + const enet_real = b.dependency("enet_real", .{}); + const enet = b.createModule(.{ + .root_source_file = .{ .path = "src/enet.zig" }, .target = target, .optimize = optimize, + .link_libc = true, }); - - // This declares intent for the library to be installed into the standard - // location when the user invokes the "install" step (the default step when - // running `zig build`). - b.installArtifact(lib); + enet.addCSourceFiles(.{ + .root = enet_real.path(""), + .files = &.{ + "callbacks.c", + "compress.c", + "host.c", + "list.c", + "packet.c", + "peer.c", + "protocol.c", + "unix.c", + "win32.c", + }, + }); + enet.addIncludePath(enet_real.path("include")); const exe = b.addExecutable(.{ .name = "learnzig", @@ -35,57 +34,30 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + exe.root_module.addImport("enet", enet); - // 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); - // Creates a step for unit testing. This only builds the test executable - // but does not run it. - const lib_unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/root.zig" }, - .target = target, - .optimize = optimize, - }); - - const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - const exe_unit_tests = b.addTest(.{ .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, }); + exe_unit_tests.root_module.addImport("enet", enet); 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_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); } diff --git a/build.zig.zon b/build.zig.zon index 8814bdd..6a4662f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,62 +1,15 @@ .{ .name = "learnzig", - // 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", - //}, + .enet_real = .{ + .url = "https://github.com/lsalzman/enet/archive/c44b7d0f7ff21edb702745e4c019d0537928c373.tar.gz", + .hash = "1220b65591d2f97033626dfff25a37926b250c7892e812397668769f84ef408cd02e", + }, }, - // 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. - // 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 = .{ - // This makes *all* files, recursively, included in this package. It is generally - // better to explicitly list the files and directories instead, to insure that - // fetching from tarballs, file system paths, and version control all result - // in the same contents hash. "", - // For example... - //"build.zig", - //"build.zig.zon", - //"src", - //"LICENSE", - //"README.md", }, } diff --git a/src/enet.zig b/src/enet.zig new file mode 100644 index 0000000..aad6c70 --- /dev/null +++ b/src/enet.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("enet/enet.h"); +}); diff --git a/src/main.zig b/src/main.zig index 6875ac0..1f30ffb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const enet = @import("enet"); const DH = std.crypto.dh.X25519; const AES = std.crypto.aead.aes_gcm.Aes256Gcm; @@ -62,6 +63,9 @@ const Packet = struct { }; pub fn main() !void { + if (enet.enet_initialize() != 0) return error.ENetInitFailed; + defer enet.enet_deinitialize(); + const bob = try Encryptor.init(); const alice = try Encryptor.init(); @@ -106,9 +110,9 @@ pub fn main() !void { std.debug.print( "Packet: {s} {s} {s}\n", .{ - try digest(ally, pkt.cipher_text, .lower), - try digest(ally, &pkt.nonce, .lower), - try digest(ally, &pkt.tag, .lower), + try digest(ally, pkt.cipher_text, .lower), + try digest(ally, &pkt.nonce, .lower), + try digest(ally, &pkt.tag, .lower), }, ); std.debug.assert(!std.mem.eql(u8, pkt.cipher_text, msg)); diff --git a/src/root.zig b/src/root.zig deleted file mode 100644 index ecfeade..0000000 --- a/src/root.zig +++ /dev/null @@ -1,10 +0,0 @@ -const std = @import("std"); -const testing = std.testing; - -export fn add(a: i32, b: i32) i32 { - return a + b; -} - -test "basic add functionality" { - try testing.expect(add(3, 7) == 10); -} From bd05dae216ffde6acc713ed4df5305a7f065a433 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 5 Mar 2024 13:38:13 -0500 Subject: [PATCH 09/11] split into client and main zig --- build.zig | 52 +++++++---- src/client.zig | 39 ++++++++ src/main.zig | 235 ------------------------------------------------- src/server.zig | 39 ++++++++ 4 files changed, 114 insertions(+), 251 deletions(-) create mode 100644 src/client.zig delete mode 100644 src/main.zig create mode 100644 src/server.zig diff --git a/build.zig b/build.zig index f4488e2..a08a760 100644 --- a/build.zig +++ b/build.zig @@ -28,36 +28,56 @@ pub fn build(b: *std.Build) void { }); enet.addIncludePath(enet_real.path("include")); - const exe = b.addExecutable(.{ - .name = "learnzig", + const client = b.addExecutable(.{ + .name = "client", .root_source_file = .{ .path = "src/main.zig" }, .target = target, .optimize = optimize, }); - exe.root_module.addImport("enet", enet); + client.root_module.addImport("enet", enet); + b.installArtifact(client); - b.installArtifact(exe); + const server = b.addExecutable(.{ + .name = "server", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + server.root_module.addImport("enet", enet); + b.installArtifact(server); - const run_cmd = b.addRunArtifact(exe); - - run_cmd.step.dependOn(b.getInstallStep()); + const client_cmd = b.addRunArtifact(client); + client_cmd.step.dependOn(b.getInstallStep()); + const server_cmd = b.addRunArtifact(server); + server_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { - run_cmd.addArgs(args); + client_cmd.addArgs(args); + server_cmd.addArgs(args); } - const run_step = b.step("run", "Run the app"); - run_step.dependOn(&run_cmd.step); + const client_step = b.step("client", "Run the client"); + client_step.dependOn(&client_cmd.step); + const server_step = b.step("server", "Run the server"); + server_step.dependOn(&server_cmd.step); - const exe_unit_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, + const client_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/client.zig" }, .target = target, .optimize = optimize, }); - exe_unit_tests.root_module.addImport("enet", enet); + client.root_module.addImport("enet", enet); + const server_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/server.zig" }, + .target = target, + .optimize = optimize, + }); + server.root_module.addImport("enet", enet); - const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + const run_client_tests = b.addRunArtifact(client_tests); + const run_server_tests = b.addRunArtifact(server_tests); - const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_exe_unit_tests.step); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_client_tests.step); + test_step.dependOn(&run_server_tests.step); } diff --git a/src/client.zig b/src/client.zig new file mode 100644 index 0000000..311c590 --- /dev/null +++ b/src/client.zig @@ -0,0 +1,39 @@ +const std = @import("std"); +const enet = @import("enet"); + +const DH = std.crypto.dh.X25519; +const AES = std.crypto.aead.aes_gcm.Aes256Gcm; +const SHA = std.crypto.hash.sha2.Sha256; + +pub fn main() !void { + if (enet.enet_initialize() != 0) return error.ENetInitFailed; + defer enet.enet_deinitialize(); +} + +// it even works in ReleaseFast so I think it's ok. +test "in-place-encrypt" { + const al = std.testing.allocator; + + var key: [AES.key_length]u8 = undefined; + std.crypto.random.bytes(&key); + const iv: [AES.nonce_length]u8 = undefined; + std.crypto.random.bytes(&key); + + var tag: [AES.tag_length]u8 = undefined; + + // 1mb message. very big. should find an error if one exists. + const original_message: []u8 = try al.alloc(u8, 1 << 20); + defer al.free(original_message); + std.crypto.random.bytes(original_message); + + const message = try al.dupe(u8, original_message); + defer al.free(message); + + const ad: []u8 = try al.dupe(u8, "u"); + defer al.free(ad); + + AES.encrypt(message, &tag, message, ad, iv, key); + try std.testing.expect(!std.mem.eql(u8, message, original_message)); + try AES.decrypt(message, message, tag, ad, iv, key); + try std.testing.expect(std.mem.eql(u8, message, original_message)); +} diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index 1f30ffb..0000000 --- a/src/main.zig +++ /dev/null @@ -1,235 +0,0 @@ -const std = @import("std"); -const enet = @import("enet"); - -const DH = std.crypto.dh.X25519; -const AES = std.crypto.aead.aes_gcm.Aes256Gcm; -const SHA = std.crypto.hash.sha2.Sha256; - -const Encryptor = struct { - const Self = @This(); - - kp: DH.KeyPair, - - pub fn init() !Self { - var self: Self = undefined; - - var seed: [DH.seed_length]u8 = undefined; - std.crypto.random.bytes(&seed); - self.kp = try DH.KeyPair.create(seed); - @memset(&seed, 0); - - return self; - } -}; - -const Peer = struct { - const Self = @This(); - - public_key: [DH.public_length]u8, - shared_key: [AES.key_length]u8, - - pub fn init(me: DH.KeyPair, peer_key: [DH.public_length]u8) !Self { - var self: Self = undefined; - self.public_key = peer_key; - - const secret = try DH.scalarmult(me.secret_key, peer_key); - SHA.hash(&secret, &self.shared_key, .{}); - - return self; - } - - /// Caller must free result - pub fn encrypt(self: Self, ally: std.mem.Allocator, msg: []const u8) !Packet { - var pkt: Packet = undefined; - std.crypto.random.bytes(&pkt.nonce); - pkt.cipher_text = try ally.alloc(u8, msg.len); - AES.encrypt(pkt.cipher_text, &pkt.tag, msg, "", pkt.nonce, self.shared_key); - return pkt; - } - - /// Caller must free result - pub fn decrypt(self: Self, ally: std.mem.Allocator, pkt: Packet) ![]const u8 { - const msg: []u8 = try ally.alloc(u8, pkt.cipher_text.len); - try AES.decrypt(msg, pkt.cipher_text, pkt.tag, "", pkt.nonce, self.shared_key); - return msg; - } -}; - -const Packet = struct { - ally: std.mem.Allocator, - nonce: [AES.nonce_length]u8, - tag: [AES.tag_length]u8, - cipher_text: []u8, -}; - -pub fn main() !void { - if (enet.enet_initialize() != 0) return error.ENetInitFailed; - defer enet.enet_deinitialize(); - - const bob = try Encryptor.init(); - const alice = try Encryptor.init(); - - std.debug.print( - "Bob sends {s}\n", - .{std.fmt.bytesToHex(bob.kp.public_key, .lower)}, - ); - std.debug.print( - "Alice sends {s}\n", - .{std.fmt.bytesToHex(alice.kp.public_key, .lower)}, - ); - - const bob_peer = try Peer.init(bob.kp, alice.kp.public_key); - const alice_peer = try Peer.init(alice.kp, bob.kp.public_key); - - std.debug.print( - "Bob deduces {s}\n", - .{std.fmt.bytesToHex(bob_peer.shared_key, .lower)}, - ); - std.debug.print( - "Alice deduces {s}\n", - .{std.fmt.bytesToHex(alice_peer.shared_key, .lower)}, - ); - - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - var arena = std.heap.ArenaAllocator.init(gpa.allocator()); - defer arena.deinit(); - - const ally = arena.allocator(); - - // bob send a message to alice - const msg = "Hello World!"; - - std.debug.print( - "Bob sending message '{s}' ({s})\n", - .{ msg, std.fmt.bytesToHex(msg, .lower) }, - ); - - const pkt = try bob_peer.encrypt(ally, msg); - defer ally.free(pkt.cipher_text); - - std.debug.print( - "Packet: {s} {s} {s}\n", - .{ - try digest(ally, pkt.cipher_text, .lower), - try digest(ally, &pkt.nonce, .lower), - try digest(ally, &pkt.tag, .lower), - }, - ); - std.debug.assert(!std.mem.eql(u8, pkt.cipher_text, msg)); - - const rcv = try alice_peer.decrypt(ally, pkt); - defer ally.free(rcv); - std.debug.print( - "Alice received '{s}' ({s})\n", - .{ rcv, try digest(ally, rcv, .lower) }, - ); - - // var seed: [dh.X25519.seed_length]u8 = undefined; - // std.crypto.random.bytes(&seed); - // const kp1 = try dh.X25519.KeyPair.create(seed); - // std.crypto.random.bytes(&seed); - // const kp2 = try dh.X25519.KeyPair.create(seed); - // - // std.debug.print("{any}\n{any}\n\n", .{ kp1.public_key, kp1.secret_key }); - // std.debug.print("{any}\n{any}\n\n", .{ kp2.public_key, kp2.secret_key }); - // - // const sec_1 = try dh.X25519.scalarmult(kp1.secret_key, kp2.public_key); - // const sec_2 = try dh.X25519.scalarmult(kp2.secret_key, kp1.public_key); - // - // var key: [aead.aes_gcm.Aes256Gcm.key_length]u8 = undefined; - // std.crypto.hash.sha2.Sha256.hash(&sec_1, &key, .{}); - // - // std.debug.print("{any}\n", .{sec_1}); - // std.debug.print("{any}\n", .{sec_2}); - // - // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - // const ally = gpa.allocator(); - // - // const m: []const u8 = "hello world!"; - // const ad: []const u8 = "foo"; - // - // // see https://crypto.stackexchange.com/a/5818 - // // the nonce does not need to be random, it can be a counter - // // it can be sent in clear, or each end may deduce it - // // but it ___MUST___ be unique and unrelated to the key - // var iv: [aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; - // std.crypto.random.bytes(&iv); - // - // const c: []u8 = try ally.alloc(u8, m.len); - // defer ally.free(c); - // - // @memset(c, 0); - // var tag: [aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; - // - // std.debug.print("c: {any}\n", .{c}); - // aead.aes_gcm.Aes256Gcm.encrypt(c, &tag, m, ad, iv, key); - // std.debug.print("{any} {any} {any}\n", .{ c, tag, iv }); - - // aead.aes_gcm.Aes256Gcm.encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) - - // var seed_a = [sign.Ed25519.KeyPair.seed_length]u8; - // std.crypto.random.bytes(&seed_a); - // const pair_a = try sign.Ed25519.KeyPair.create(seed_a); - // var pubkey_a = dh.X25519.publicKeyFromEd25519(pair_a.public_key); - // - // var seed_b = [sign.Ed25519.KeyPair.seed_length]u8; - // std.crypto.random.bytes(&seed_b); - // const pair_b = try sign.Ed25519.KeyPair.create(seed_b); - // var pubkey_b = try dh.X25519.publicKeyFromEd25519(pair_b.public_key); - - // var buf: [64]u8 = undefined; - // @memset(&buf, 0); - // std.debug.print("{any}\n", .{buf}); - // std.crypto.random.bytes(&buf); - // std.debug.print("{any}\n", .{buf}); -} - -test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); -} - -/// caller takes ownership of result -fn digest(ally: std.mem.Allocator, data: []const u8, case: std.fmt.Case) ![]const u8 { - const charset = switch (case) { - .lower => "0123456789abcdef", - .upper => "0123456789ABCDEF", - }; - - var res = try ally.alloc(u8, data.len * 2); - for (data, 0..) |ch, idx| { - res[idx * 2 + 0] = charset[ch >> 4]; - res[idx * 2 + 1] = charset[ch & 0xf]; - } - return res; -} - -// it even works in ReleaseFast so I think it's ok. -test "in-place-encrypt" { - const al = std.testing.allocator; - - var key: [AES.key_length]u8 = undefined; - std.crypto.random.bytes(&key); - const iv: [AES.nonce_length]u8 = undefined; - std.crypto.random.bytes(&key); - - var tag: [AES.tag_length]u8 = undefined; - - // 1mb message. very big. should find an error if one exists. - const original_message: []u8 = try al.alloc(u8, 1 << 20); - defer al.free(original_message); - std.crypto.random.bytes(original_message); - - const message = try al.dupe(u8, original_message); - defer al.free(message); - - const ad: []u8 = try al.dupe(u8, "u"); - defer al.free(ad); - - AES.encrypt(message, &tag, message, ad, iv, key); - try std.testing.expect(!std.mem.eql(u8, message, original_message)); - try AES.decrypt(message, message, tag, ad, iv, key); - try std.testing.expect(std.mem.eql(u8, message, original_message)); -} diff --git a/src/server.zig b/src/server.zig new file mode 100644 index 0000000..311c590 --- /dev/null +++ b/src/server.zig @@ -0,0 +1,39 @@ +const std = @import("std"); +const enet = @import("enet"); + +const DH = std.crypto.dh.X25519; +const AES = std.crypto.aead.aes_gcm.Aes256Gcm; +const SHA = std.crypto.hash.sha2.Sha256; + +pub fn main() !void { + if (enet.enet_initialize() != 0) return error.ENetInitFailed; + defer enet.enet_deinitialize(); +} + +// it even works in ReleaseFast so I think it's ok. +test "in-place-encrypt" { + const al = std.testing.allocator; + + var key: [AES.key_length]u8 = undefined; + std.crypto.random.bytes(&key); + const iv: [AES.nonce_length]u8 = undefined; + std.crypto.random.bytes(&key); + + var tag: [AES.tag_length]u8 = undefined; + + // 1mb message. very big. should find an error if one exists. + const original_message: []u8 = try al.alloc(u8, 1 << 20); + defer al.free(original_message); + std.crypto.random.bytes(original_message); + + const message = try al.dupe(u8, original_message); + defer al.free(message); + + const ad: []u8 = try al.dupe(u8, "u"); + defer al.free(ad); + + AES.encrypt(message, &tag, message, ad, iv, key); + try std.testing.expect(!std.mem.eql(u8, message, original_message)); + try AES.decrypt(message, message, tag, ad, iv, key); + try std.testing.expect(std.mem.eql(u8, message, original_message)); +} From e9663d62faebbfea29153c4b4ce2343e7e7157cf Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 5 Mar 2024 17:23:32 -0500 Subject: [PATCH 10/11] working enet connection --- build.zig | 10 +++++----- src/client.zig | 30 ++++++++++++++++++++++++++++++ src/enet.zig | 2 ++ src/server.zig | 22 ++++++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index a08a760..3141ec0 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,6 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); const enet_real = b.dependency("enet_real", .{}); @@ -11,6 +10,7 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, .link_libc = true, + .sanitize_c = false, }); enet.addCSourceFiles(.{ .root = enet_real.path(""), @@ -30,7 +30,7 @@ pub fn build(b: *std.Build) void { const client = b.addExecutable(.{ .name = "client", - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "src/client.zig" }, .target = target, .optimize = optimize, }); @@ -39,7 +39,7 @@ pub fn build(b: *std.Build) void { const server = b.addExecutable(.{ .name = "server", - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "src/server.zig" }, .target = target, .optimize = optimize, }); @@ -66,13 +66,13 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - client.root_module.addImport("enet", enet); + client_tests.root_module.addImport("enet", enet); const server_tests = b.addTest(.{ .root_source_file = .{ .path = "src/server.zig" }, .target = target, .optimize = optimize, }); - server.root_module.addImport("enet", enet); + server_tests.root_module.addImport("enet", enet); const run_client_tests = b.addRunArtifact(client_tests); const run_server_tests = b.addRunArtifact(server_tests); diff --git a/src/client.zig b/src/client.zig index 311c590..33a457b 100644 --- a/src/client.zig +++ b/src/client.zig @@ -8,6 +8,36 @@ const SHA = std.crypto.hash.sha2.Sha256; pub fn main() !void { if (enet.enet_initialize() != 0) return error.ENetInitFailed; defer enet.enet_deinitialize(); + + const addr = enet.ENetAddress{ + .host = enet.ENET_HOST_ANY, + .port = enet.ENET_PORT_ANY, + }; + const host = enet.enet_host_create(&addr, 32, 1, 0, 0) orelse return error.ENetHostCreateFailed; + defer enet.enet_host_destroy(host); + + var server_addr = enet.ENetAddress{ + .host = enet.ENET_HOST_ANY, + .port = 9405, + }; + _ = enet.enet_address_set_host(&server_addr, "localhost"); + const peer = enet.enet_host_connect(host, &server_addr, 1, 0); + _ = peer; + + var event: enet.ENetEvent = undefined; + + while (true) { + while (enet.enet_host_service(host, &event, 100) > 0) { + switch (event.type) { + enet.ENET_EVENT_TYPE_CONNECT => { + std.debug.print("client: connected to peer {any}\n", .{event.peer.*.address}); + }, + else => { + std.debug.print("client: other event...\n", .{}); + }, + } + } + } } // it even works in ReleaseFast so I think it's ok. diff --git a/src/enet.zig b/src/enet.zig index aad6c70..016ab6d 100644 --- a/src/enet.zig +++ b/src/enet.zig @@ -1,3 +1,5 @@ +const c = @This(); + pub usingnamespace @cImport({ @cInclude("enet/enet.h"); }); diff --git a/src/server.zig b/src/server.zig index 311c590..4ed311e 100644 --- a/src/server.zig +++ b/src/server.zig @@ -8,6 +8,28 @@ const SHA = std.crypto.hash.sha2.Sha256; pub fn main() !void { if (enet.enet_initialize() != 0) return error.ENetInitFailed; defer enet.enet_deinitialize(); + + const addr = enet.ENetAddress{ + .host = enet.ENET_HOST_ANY, + .port = 9405, + }; + const host = enet.enet_host_create(&addr, 32, 1, 0, 0) orelse return error.ENetHostCreateFailed; + defer enet.enet_host_destroy(host); + + var event: enet.ENetEvent = undefined; + + while (true) { + while (enet.enet_host_service(host, &event, 100) > 0) { + switch (event.type) { + enet.ENET_EVENT_TYPE_CONNECT => { + std.debug.print("server: connected to peer {any}\n", .{event.peer.*.address}); + }, + else => { + std.debug.print("server: other event...\n", .{}); + }, + } + } + } } // it even works in ReleaseFast so I think it's ok. From 0f148031e36547e44c6b9a41d443a0d8735af14c Mon Sep 17 00:00:00 2001 From: David Allemang Date: Tue, 5 Mar 2024 17:24:41 -0500 Subject: [PATCH 11/11] add todo about serve wrapper --- src/enet.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/enet.zig b/src/enet.zig index 016ab6d..1aa9def 100644 --- a/src/enet.zig +++ b/src/enet.zig @@ -3,3 +3,5 @@ const c = @This(); pub usingnamespace @cImport({ @cInclude("enet/enet.h"); }); + +// todo add "serve" here that invokes some callback with events.