├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── src ├── clap.zig ├── clap │ ├── args.zig │ ├── codepoint_counting_writer.zig │ ├── parsers.zig │ └── streaming.zig ├── lib.zig ├── main.zig └── wasm.zig └── test_compatibility.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-cache 3 | zig-out 4 | *~ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * ISC License 3 | * 4 | * Copyright (c) 2023-2025 5 | * Frank Denis 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 14 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 17 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zig-minisign 2 | 3 | A Zig implementation of [Minisign](https://jedisct1.github.io/minisign/). 4 | 5 | `minizign` supports signature verification, signing, and key generation. 6 | 7 | ## Compilation 8 | 9 | Requires the current `master` version of [Zig](https://ziglang.org). 10 | 11 | Compile with: 12 | 13 | ```sh 14 | zig build -Doptimize=ReleaseSmall 15 | ``` 16 | 17 | for a size-optimized version, or 18 | 19 | ```sh 20 | zig build -Doptimize=ReleaseFast 21 | ``` 22 | 23 | for a speed-optimized version. 24 | 25 | ## Usage 26 | 27 | ```text 28 | Usage: 29 | -h, --help Display this help and exit 30 | -p, --publickey-path Public key path to a file 31 | -P, --publickey Public key, as a BASE64-encoded string 32 | -s, --secretkey-path Secret key path to a file 33 | -l, --legacy Accept legacy signatures 34 | -m, --input Input file 35 | -o, --output Output file (signature) 36 | -q, --quiet Quiet mode 37 | -V, --verify Verify 38 | -S, --sign Sign 39 | -G, --generate Generate a new key pair 40 | -C, --convert Convert the given public key to SSH format 41 | -t, --trusted-comment Trusted comment 42 | -c, --untrusted-comment Untrusted comment 43 | ``` 44 | 45 | ## Examples 46 | 47 | ### Key Generation 48 | 49 | Generate a new key pair: 50 | 51 | ```sh 52 | minizign -G -s minisign.key -p minisign.pub 53 | ``` 54 | 55 | This will prompt for a password to encrypt the secret key. Leave empty for an unencrypted key. 56 | 57 | ### Signing 58 | 59 | Sign a file: 60 | 61 | ```sh 62 | minizign -S -s minisign.key -m file.txt 63 | ``` 64 | 65 | This creates `file.txt.minisig`. You can specify a custom output path with `-o`. 66 | 67 | ### Verification 68 | 69 | Verify `public-resolvers.md` using `public-resolvers.md.minisig` and the public key file `minisign.pub`: 70 | 71 | ```sh 72 | minizign -V -p minisign.pub -m public-resolvers.md 73 | ``` 74 | 75 | Verify `public-resolvers.md` by directly providing the public key on the command-line: 76 | 77 | ```sh 78 | minizign -V -P RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3 -m public-resolvers.md 79 | ``` 80 | 81 | ## SSH-encoded public keys 82 | 83 | `minizign` can encode public keys in SSH format, so that they can be uploaded to GitHub: 84 | 85 | ```sh 86 | minizign -p minisign.pub -C 87 | ``` 88 | 89 | ```text 90 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHmlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3 minisign key E7620F1842B4E81F 91 | ``` 92 | 93 | GitHub makes public SSH keys available at `https://github.com/.keys`. 94 | 95 | SSH-encoded keys can be loaded by `minizign` the same way as native keys, with `-p `. They will be automatically recognized as such. 96 | 97 | ## Features 98 | 99 | `minizign` supports prehashing (which can be forced if you know this is how the signature was created), has zero dependencies and can be cross-compiled to anything that Zig can cross-compile to, including WebAssembly. 100 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | // Standard target options allows the person running `zig build` to choose 5 | // what target to build for. Here we do not override the defaults, which 6 | // means any target is allowed, and the default is native. Other options 7 | // for restricting supported target set are available. 8 | const target = b.standardTargetOptions(.{}); 9 | 10 | // Standard optimization options allow the person running `zig build` to select 11 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 12 | // set a preferred release mode, allowing the user to decide how to optimize. 13 | const optimize = b.standardOptimizeOption(.{}); 14 | 15 | const minizign_module = b.addModule("minizign", .{ 16 | .root_source_file = b.path("src/lib.zig"), 17 | .target = target, 18 | .optimize = optimize, 19 | }); 20 | 21 | const resolved_target = target.result; 22 | const is_freestanding = resolved_target.os.tag == .freestanding; 23 | 24 | // Skip building the CLI executable for freestanding targets (e.g., WASM) 25 | // since it requires OS features like file I/O, stdin/stdout, and process control 26 | if (!is_freestanding) { 27 | // Build minzign cli 28 | const exe = b.addExecutable(.{ 29 | .name = "minizign", 30 | .root_module = b.createModule(.{ 31 | .root_source_file = b.path("src/main.zig"), 32 | .target = target, 33 | .optimize = optimize, 34 | }), 35 | }); 36 | 37 | exe.root_module.addImport("minizign", minizign_module); 38 | 39 | b.installArtifact(exe); 40 | 41 | const run_cmd = b.addRunArtifact(exe); 42 | run_cmd.step.dependOn(b.getInstallStep()); 43 | 44 | if (b.args) |args| { 45 | run_cmd.addArgs(args); 46 | } 47 | 48 | const run_step = b.step("run", "Run the app"); 49 | run_step.dependOn(&run_cmd.step); 50 | } 51 | 52 | const lib_unit_tests = b.addTest(.{ 53 | .root_module = minizign_module, 54 | }); 55 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 56 | const test_step = b.step("test", "Run unit tests"); 57 | test_step.dependOn(&run_lib_unit_tests.step); 58 | 59 | // Build a wasm32-freestanding module when the target is wasm32-freestanding 60 | if (is_freestanding and resolved_target.cpu.arch == .wasm32) { 61 | const wasm = b.addExecutable(.{ 62 | .name = "minizign", 63 | .root_module = b.createModule(.{ 64 | .root_source_file = b.path("src/wasm.zig"), 65 | .target = target, 66 | .optimize = optimize, 67 | }), 68 | }); 69 | 70 | wasm.root_module.addImport("minizign", minizign_module); 71 | 72 | wasm.entry = .disabled; 73 | wasm.export_memory = true; 74 | wasm.root_module.export_symbol_names = &.{ 75 | "allocate", 76 | "free", 77 | "signatureDecode", 78 | "signatureGetTrustedComment", 79 | "signatureGetTrustedCommentLength", 80 | "signatureDeinit", 81 | "publicKeyDecodeFromBase64", 82 | "publicKeyDecodeFromSsh", 83 | "publicKeyDeinit", 84 | "publicKeyVerifier", 85 | "verifierUpdate", 86 | "verifierVerify", 87 | "verifierDeinit", 88 | }; 89 | 90 | const installWasm = b.addInstallArtifact(wasm, .{}); 91 | b.getInstallStep().dependOn(&installWasm.step); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .minizign, 3 | .version = "0.1.7", 4 | .minimum_zig_version = "0.15.1", 5 | .fingerprint = 0x550c194b717c0d82, 6 | .paths = .{ 7 | "LICENSE", 8 | "README.md", 9 | "build.zig", 10 | "build.zig.zon", 11 | "src", 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /src/clap.zig: -------------------------------------------------------------------------------- 1 | pub const default_assignment_separators = "="; 2 | 3 | /// The names a `Param` can have. 4 | pub const Names = struct { 5 | /// '-' prefix 6 | short: ?u8 = null, 7 | 8 | /// '--' prefix 9 | long: ?[]const u8 = null, 10 | 11 | /// The longest of the possible names this `Names` struct can represent. 12 | pub fn longest(names: *const Names) Longest { 13 | if (names.long) |long| 14 | return .{ .kind = .long, .name = long }; 15 | if (names.short) |*short| 16 | return .{ .kind = .short, .name = @as(*const [1]u8, short) }; 17 | 18 | return .{ .kind = .positional, .name = "" }; 19 | } 20 | 21 | pub const Longest = struct { 22 | kind: Kind, 23 | name: []const u8, 24 | }; 25 | 26 | pub const Kind = enum { 27 | long, 28 | short, 29 | positional, 30 | 31 | pub fn prefix(kind: Kind) []const u8 { 32 | return switch (kind) { 33 | .long => "--", 34 | .short => "-", 35 | .positional => "", 36 | }; 37 | } 38 | }; 39 | }; 40 | 41 | /// Whether a param takes no value (a flag), one value, or can be specified multiple times. 42 | pub const Values = enum { 43 | none, 44 | one, 45 | many, 46 | }; 47 | 48 | /// Represents a parameter for the command line. 49 | /// Parameters come in three kinds: 50 | /// * Short ("-a"): Should be used for the most commonly used parameters in your program. 51 | /// * They can take a value three different ways. 52 | /// * "-a value" 53 | /// * "-a=value" 54 | /// * "-avalue" 55 | /// * They chain if they don't take values: "-abc". 56 | /// * The last given parameter can take a value in the same way that a single parameter can: 57 | /// * "-abc value" 58 | /// * "-abc=value" 59 | /// * "-abcvalue" 60 | /// * Long ("--long-param"): Should be used for less common parameters, or when no single 61 | /// character can describe the parameter. 62 | /// * They can take a value two different ways. 63 | /// * "--long-param value" 64 | /// * "--long-param=value" 65 | /// * Positional: Should be used as the primary parameter of the program, like a filename or 66 | /// an expression to parse. 67 | /// * Positional parameters have both names.long and names.short == null. 68 | /// * Positional parameters must take a value. 69 | pub fn Param(comptime Id: type) type { 70 | return struct { 71 | id: Id, 72 | names: Names = Names{}, 73 | takes_value: Values = .none, 74 | }; 75 | } 76 | 77 | /// Takes a string and parses it into many Param(Help). Returned is a newly allocated slice 78 | /// containing all the parsed params. The caller is responsible for freeing the slice. 79 | pub fn parseParams(allocator: std.mem.Allocator, str: []const u8) ![]Param(Help) { 80 | var end: usize = undefined; 81 | return parseParamsEx(allocator, str, &end); 82 | } 83 | 84 | /// Takes a string and parses it into many Param(Help). Returned is a newly allocated slice 85 | /// containing all the parsed params. The caller is responsible for freeing the slice. 86 | pub fn parseParamsEx(allocator: std.mem.Allocator, str: []const u8, end: *usize) ![]Param(Help) { 87 | var list = std.ArrayList(Param(Help)){}; 88 | errdefer list.deinit(allocator); 89 | 90 | try parseParamsIntoArrayListEx(allocator, &list, str, end); 91 | return try list.toOwnedSlice(allocator); 92 | } 93 | 94 | /// Takes a string and parses it into many Param(Help) at comptime. Returned is an array of 95 | /// exactly the number of params that was parsed from `str`. A parse error becomes a compiler 96 | /// error. 97 | pub fn parseParamsComptime(comptime str: []const u8) [countParams(str)]Param(Help) { 98 | var end: usize = undefined; 99 | var res: [countParams(str)]Param(Help) = undefined; 100 | _ = parseParamsIntoSliceEx(&res, str, &end) catch { 101 | const loc = std.zig.findLineColumn(str, end); 102 | @compileError(std.fmt.comptimePrint("error:{}:{}: Failed to parse parameter:\n{s}", .{ 103 | loc.line + 1, 104 | loc.column + 1, 105 | loc.source_line, 106 | })); 107 | }; 108 | return res; 109 | } 110 | 111 | fn countParams(str: []const u8) usize { 112 | // See parseParamEx for reasoning. I would like to remove it from parseParam, but people 113 | // depend on that function to still work conveniently at comptime, so leaving it for now. 114 | @setEvalBranchQuota(std.math.maxInt(u32)); 115 | 116 | var res: usize = 0; 117 | var it = std.mem.splitScalar(u8, str, '\n'); 118 | while (it.next()) |line| { 119 | const trimmed = std.mem.trimLeft(u8, line, " \t"); 120 | if (std.mem.startsWith(u8, trimmed, "-") or 121 | std.mem.startsWith(u8, trimmed, "<")) 122 | { 123 | res += 1; 124 | } 125 | } 126 | 127 | return res; 128 | } 129 | 130 | /// Takes a string and parses it into many Param(Help), which are written to `slice`. A subslice 131 | /// is returned, containing all the parameters parsed. This function will fail if the input slice 132 | /// is to small. 133 | pub fn parseParamsIntoSlice(slice: []Param(Help), str: []const u8) ![]Param(Help) { 134 | var list = std.ArrayList(Param(Help)){ 135 | .items = slice[0..0], 136 | .capacity = slice.len, 137 | }; 138 | 139 | try parseParamsIntoArrayList(&list, str); 140 | return list.items; 141 | } 142 | 143 | /// Takes a string and parses it into many Param(Help), which are written to `slice`. A subslice 144 | /// is returned, containing all the parameters parsed. This function will fail if the input slice 145 | /// is to small. 146 | pub fn parseParamsIntoSliceEx(slice: []Param(Help), str: []const u8, end: *usize) ![]Param(Help) { 147 | var null_allocator = std.heap.FixedBufferAllocator.init(""); 148 | var list = std.ArrayList(Param(Help)){ 149 | .items = slice[0..0], 150 | .capacity = slice.len, 151 | }; 152 | 153 | try parseParamsIntoArrayListEx(null_allocator.allocator(), &list, str, end); 154 | return list.items; 155 | } 156 | 157 | /// Takes a string and parses it into many Param(Help), which are appended onto `list`. 158 | pub fn parseParamsIntoArrayList(list: *std.ArrayList(Param(Help)), str: []const u8) !void { 159 | var end: usize = undefined; 160 | return parseParamsIntoArrayListEx(list, str, &end); 161 | } 162 | 163 | /// Takes a string and parses it into many Param(Help), which are appended onto `list`. 164 | pub fn parseParamsIntoArrayListEx(allocator: std.mem.Allocator, list: *std.ArrayList(Param(Help)), str: []const u8, end: *usize) !void { 165 | var i: usize = 0; 166 | while (i != str.len) { 167 | var end_of_this: usize = undefined; 168 | errdefer end.* = i + end_of_this; 169 | 170 | try list.append(allocator, try parseParamEx(str[i..], &end_of_this)); 171 | i += end_of_this; 172 | } 173 | 174 | end.* = str.len; 175 | } 176 | 177 | pub fn parseParam(str: []const u8) !Param(Help) { 178 | var end: usize = undefined; 179 | return parseParamEx(str, &end); 180 | } 181 | 182 | /// Takes a string and parses it to a Param(Help). 183 | pub fn parseParamEx(str: []const u8, end: *usize) !Param(Help) { 184 | // This function become a lot less ergonomic to use once you hit the eval branch quota. To 185 | // avoid this we pick a sane default. Sadly, the only sane default is the biggest possible 186 | // value. If we pick something a lot smaller and a user hits the quota after that, they have 187 | // no way of overriding it, since we set it here. 188 | // We can recosider this again if: 189 | // * We get parseParams: https://github.com/Hejsil/zig-clap/issues/39 190 | // * We get a larger default branch quota in the zig compiler (stage 2). 191 | // * Someone points out how this is a really bad idea. 192 | @setEvalBranchQuota(std.math.maxInt(u32)); 193 | 194 | var res = Param(Help){ .id = .{} }; 195 | var start: usize = 0; 196 | var state: enum { 197 | start, 198 | 199 | start_of_short_name, 200 | end_of_short_name, 201 | 202 | before_long_name_or_value_or_description, 203 | 204 | before_long_name, 205 | start_of_long_name, 206 | first_char_of_long_name, 207 | rest_of_long_name, 208 | 209 | before_value_or_description, 210 | 211 | first_char_of_value, 212 | rest_of_value, 213 | end_of_one_value, 214 | second_dot_of_multi_value, 215 | third_dot_of_multi_value, 216 | 217 | before_description, 218 | rest_of_description, 219 | rest_of_description_new_line, 220 | } = .start; 221 | for (str, 0..) |c, i| { 222 | errdefer end.* = i; 223 | 224 | switch (state) { 225 | .start => switch (c) { 226 | ' ', '\t', '\n' => {}, 227 | '-' => state = .start_of_short_name, 228 | '<' => state = .first_char_of_value, 229 | else => return error.InvalidParameter, 230 | }, 231 | 232 | .start_of_short_name => switch (c) { 233 | '-' => state = .first_char_of_long_name, 234 | 'a'...'z', 'A'...'Z', '0'...'9' => { 235 | res.names.short = c; 236 | state = .end_of_short_name; 237 | }, 238 | else => return error.InvalidParameter, 239 | }, 240 | .end_of_short_name => switch (c) { 241 | ' ', '\t' => state = .before_long_name_or_value_or_description, 242 | '\n' => { 243 | start = i + 1; 244 | end.* = i + 1; 245 | state = .rest_of_description_new_line; 246 | }, 247 | ',' => state = .before_long_name, 248 | else => return error.InvalidParameter, 249 | }, 250 | 251 | .before_long_name => switch (c) { 252 | ' ', '\t' => {}, 253 | '-' => state = .start_of_long_name, 254 | else => return error.InvalidParameter, 255 | }, 256 | .start_of_long_name => switch (c) { 257 | '-' => state = .first_char_of_long_name, 258 | else => return error.InvalidParameter, 259 | }, 260 | .first_char_of_long_name => switch (c) { 261 | 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => { 262 | start = i; 263 | state = .rest_of_long_name; 264 | }, 265 | else => return error.InvalidParameter, 266 | }, 267 | .rest_of_long_name => switch (c) { 268 | 'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {}, 269 | ' ', '\t' => { 270 | res.names.long = str[start..i]; 271 | state = .before_value_or_description; 272 | }, 273 | '\n' => { 274 | res.names.long = str[start..i]; 275 | start = i + 1; 276 | end.* = i + 1; 277 | state = .rest_of_description_new_line; 278 | }, 279 | else => return error.InvalidParameter, 280 | }, 281 | 282 | .before_long_name_or_value_or_description => switch (c) { 283 | ' ', '\t' => {}, 284 | ',' => state = .before_long_name, 285 | '<' => state = .first_char_of_value, 286 | else => { 287 | start = i; 288 | state = .rest_of_description; 289 | }, 290 | }, 291 | 292 | .before_value_or_description => switch (c) { 293 | ' ', '\t' => {}, 294 | '<' => state = .first_char_of_value, 295 | else => { 296 | start = i; 297 | state = .rest_of_description; 298 | }, 299 | }, 300 | .first_char_of_value => switch (c) { 301 | '>' => return error.InvalidParameter, 302 | else => { 303 | start = i; 304 | state = .rest_of_value; 305 | }, 306 | }, 307 | .rest_of_value => switch (c) { 308 | '>' => { 309 | res.takes_value = .one; 310 | res.id.val = str[start..i]; 311 | state = .end_of_one_value; 312 | }, 313 | else => {}, 314 | }, 315 | .end_of_one_value => switch (c) { 316 | '.' => state = .second_dot_of_multi_value, 317 | ' ', '\t' => state = .before_description, 318 | '\n' => { 319 | start = i + 1; 320 | end.* = i + 1; 321 | state = .rest_of_description_new_line; 322 | }, 323 | else => { 324 | start = i; 325 | state = .rest_of_description; 326 | }, 327 | }, 328 | .second_dot_of_multi_value => switch (c) { 329 | '.' => state = .third_dot_of_multi_value, 330 | else => return error.InvalidParameter, 331 | }, 332 | .third_dot_of_multi_value => switch (c) { 333 | '.' => { 334 | res.takes_value = .many; 335 | state = .before_description; 336 | }, 337 | else => return error.InvalidParameter, 338 | }, 339 | 340 | .before_description => switch (c) { 341 | ' ', '\t' => {}, 342 | '\n' => { 343 | start = i + 1; 344 | end.* = i + 1; 345 | state = .rest_of_description_new_line; 346 | }, 347 | else => { 348 | start = i; 349 | state = .rest_of_description; 350 | }, 351 | }, 352 | .rest_of_description => switch (c) { 353 | '\n' => { 354 | end.* = i; 355 | state = .rest_of_description_new_line; 356 | }, 357 | else => {}, 358 | }, 359 | .rest_of_description_new_line => switch (c) { 360 | ' ', '\t', '\n' => {}, 361 | '-', '<' => { 362 | res.id.desc = str[start..end.*]; 363 | end.* = i; 364 | break; 365 | }, 366 | else => state = .rest_of_description, 367 | }, 368 | } 369 | } else { 370 | defer end.* = str.len; 371 | switch (state) { 372 | .rest_of_description => res.id.desc = str[start..], 373 | .rest_of_description_new_line => res.id.desc = str[start..end.*], 374 | .rest_of_long_name => res.names.long = str[start..], 375 | .end_of_short_name, 376 | .end_of_one_value, 377 | .before_value_or_description, 378 | .before_description, 379 | => {}, 380 | else => return error.InvalidParameter, 381 | } 382 | } 383 | 384 | return res; 385 | } 386 | 387 | fn testParseParams(str: []const u8, expected_params: []const Param(Help)) !void { 388 | var end: usize = undefined; 389 | const actual_params = parseParamsEx(std.testing.allocator, str, &end) catch |err| { 390 | const loc = std.zig.findLineColumn(str, end); 391 | std.debug.print("error:{}:{}: Failed to parse parameter:\n{s}\n", .{ 392 | loc.line + 1, 393 | loc.column + 1, 394 | loc.source_line, 395 | }); 396 | return err; 397 | }; 398 | defer std.testing.allocator.free(actual_params); 399 | 400 | try std.testing.expectEqual(expected_params.len, actual_params.len); 401 | for (expected_params, 0..) |_, i| 402 | try expectParam(expected_params[i], actual_params[i]); 403 | } 404 | 405 | fn expectParam(expect: Param(Help), actual: Param(Help)) !void { 406 | try std.testing.expectEqualStrings(expect.id.desc, actual.id.desc); 407 | try std.testing.expectEqualStrings(expect.id.val, actual.id.val); 408 | try std.testing.expectEqual(expect.names.short, actual.names.short); 409 | try std.testing.expectEqual(expect.takes_value, actual.takes_value); 410 | if (expect.names.long) |long| { 411 | try std.testing.expectEqualStrings(long, actual.names.long.?); 412 | } else { 413 | try std.testing.expectEqual(@as(?[]const u8, null), actual.names.long); 414 | } 415 | } 416 | 417 | test "parseParams" { 418 | try testParseParams( 419 | \\-s 420 | \\--str 421 | \\--str-str 422 | \\--str_str 423 | \\-s, --str 424 | \\--str 425 | \\-s, --str 426 | \\-s, --long Help text 427 | \\-s, --long ... Help text 428 | \\--long Help text 429 | \\-s Help text 430 | \\-s, --long Help text 431 | \\-s Help text 432 | \\--long Help text 433 | \\--long Help text 434 | \\ Help text 435 | \\... Help text 436 | \\--aa 437 | \\ This is 438 | \\ help spanning multiple 439 | \\ lines 440 | \\--aa This msg should end and the newline cause of new param 441 | \\--bb This should be a new param 442 | \\ 443 | , &.{ 444 | .{ .id = .{}, .names = .{ .short = 's' } }, 445 | .{ .id = .{}, .names = .{ .long = "str" } }, 446 | .{ .id = .{}, .names = .{ .long = "str-str" } }, 447 | .{ .id = .{}, .names = .{ .long = "str_str" } }, 448 | .{ .id = .{}, .names = .{ .short = 's', .long = "str" } }, 449 | .{ 450 | .id = .{ .val = "str" }, 451 | .names = .{ .long = "str" }, 452 | .takes_value = .one, 453 | }, 454 | .{ 455 | .id = .{ .val = "str" }, 456 | .names = .{ .short = 's', .long = "str" }, 457 | .takes_value = .one, 458 | }, 459 | .{ 460 | .id = .{ .desc = "Help text", .val = "val" }, 461 | .names = .{ .short = 's', .long = "long" }, 462 | .takes_value = .one, 463 | }, 464 | .{ 465 | .id = .{ .desc = "Help text", .val = "val" }, 466 | .names = .{ .short = 's', .long = "long" }, 467 | .takes_value = .many, 468 | }, 469 | .{ 470 | .id = .{ .desc = "Help text", .val = "val" }, 471 | .names = .{ .long = "long" }, 472 | .takes_value = .one, 473 | }, 474 | .{ 475 | .id = .{ .desc = "Help text", .val = "val" }, 476 | .names = .{ .short = 's' }, 477 | .takes_value = .one, 478 | }, 479 | .{ 480 | .id = .{ .desc = "Help text" }, 481 | .names = .{ .short = 's', .long = "long" }, 482 | }, 483 | .{ 484 | .id = .{ .desc = "Help text" }, 485 | .names = .{ .short = 's' }, 486 | }, 487 | .{ 488 | .id = .{ .desc = "Help text" }, 489 | .names = .{ .long = "long" }, 490 | }, 491 | .{ 492 | .id = .{ .desc = "Help text", .val = "A | B" }, 493 | .names = .{ .long = "long" }, 494 | .takes_value = .one, 495 | }, 496 | .{ 497 | .id = .{ .desc = "Help text", .val = "A" }, 498 | .takes_value = .one, 499 | }, 500 | .{ 501 | .id = .{ .desc = "Help text", .val = "A" }, 502 | .names = .{}, 503 | .takes_value = .many, 504 | }, 505 | .{ 506 | .id = .{ 507 | .desc = 508 | \\ This is 509 | \\ help spanning multiple 510 | \\ lines 511 | , 512 | }, 513 | .names = .{ .long = "aa" }, 514 | .takes_value = .none, 515 | }, 516 | .{ 517 | .id = .{ .desc = "This msg should end and the newline cause of new param" }, 518 | .names = .{ .long = "aa" }, 519 | .takes_value = .none, 520 | }, 521 | .{ 522 | .id = .{ .desc = "This should be a new param" }, 523 | .names = .{ .long = "bb" }, 524 | .takes_value = .none, 525 | }, 526 | }); 527 | 528 | try std.testing.expectError(error.InvalidParameter, parseParam("--long, Help")); 529 | try std.testing.expectError(error.InvalidParameter, parseParam("-s, Help")); 530 | try std.testing.expectError(error.InvalidParameter, parseParam("-ss Help")); 531 | try std.testing.expectError(error.InvalidParameter, parseParam("-ss Help")); 532 | try std.testing.expectError(error.InvalidParameter, parseParam("- Help")); 533 | } 534 | 535 | /// Optional diagnostics used for reporting useful errors 536 | pub const Diagnostic = struct { 537 | arg: []const u8 = "", 538 | name: Names = Names{}, 539 | 540 | /// Default diagnostics reporter when all you want is English with no colors. 541 | /// Use this as a reference for implementing your own if needed. 542 | pub fn report(diag: Diagnostic, stream: *std.Io.Writer, err: anyerror) !void { 543 | var longest = diag.name.longest(); 544 | if (longest.kind == .positional) 545 | longest.name = diag.arg; 546 | 547 | switch (err) { 548 | streaming.Error.DoesntTakeValue => try stream.print( 549 | "The argument '{s}{s}' does not take a value\n", 550 | .{ longest.kind.prefix(), longest.name }, 551 | ), 552 | streaming.Error.MissingValue => try stream.print( 553 | "The argument '{s}{s}' requires a value but none was supplied\n", 554 | .{ longest.kind.prefix(), longest.name }, 555 | ), 556 | streaming.Error.InvalidArgument => try stream.print( 557 | "Invalid argument '{s}{s}'\n", 558 | .{ longest.kind.prefix(), longest.name }, 559 | ), 560 | else => try stream.print("Error while parsing arguments: {s}\n", .{@errorName(err)}), 561 | } 562 | } 563 | 564 | /// Wrapper around `report`, which writes to a file in a buffered manner 565 | pub fn reportToFile(diag: Diagnostic, file: std.fs.File, err: anyerror) !void { 566 | var buf: [1024]u8 = undefined; 567 | var writer = file.writer(&buf); 568 | try diag.report(&writer.interface, err); 569 | return writer.interface.flush(); 570 | } 571 | }; 572 | 573 | fn testDiag(diag: Diagnostic, err: anyerror, expected: []const u8) !void { 574 | var buf: [1024]u8 = undefined; 575 | var writer = std.Io.Writer.fixed(&buf); 576 | try diag.report(&writer, err); 577 | try std.testing.expectEqualStrings(expected, writer.buffered()); 578 | } 579 | 580 | test "Diagnostic.report" { 581 | try testDiag(.{ .arg = "c" }, error.InvalidArgument, "Invalid argument 'c'\n"); 582 | try testDiag( 583 | .{ .name = .{ .long = "cc" } }, 584 | error.InvalidArgument, 585 | "Invalid argument '--cc'\n", 586 | ); 587 | try testDiag( 588 | .{ .name = .{ .short = 'c' } }, 589 | error.DoesntTakeValue, 590 | "The argument '-c' does not take a value\n", 591 | ); 592 | try testDiag( 593 | .{ .name = .{ .long = "cc" } }, 594 | error.DoesntTakeValue, 595 | "The argument '--cc' does not take a value\n", 596 | ); 597 | try testDiag( 598 | .{ .name = .{ .short = 'c' } }, 599 | error.MissingValue, 600 | "The argument '-c' requires a value but none was supplied\n", 601 | ); 602 | try testDiag( 603 | .{ .name = .{ .long = "cc" } }, 604 | error.MissingValue, 605 | "The argument '--cc' requires a value but none was supplied\n", 606 | ); 607 | try testDiag( 608 | .{ .name = .{ .short = 'c' } }, 609 | error.InvalidArgument, 610 | "Invalid argument '-c'\n", 611 | ); 612 | try testDiag( 613 | .{ .name = .{ .long = "cc" } }, 614 | error.InvalidArgument, 615 | "Invalid argument '--cc'\n", 616 | ); 617 | try testDiag( 618 | .{ .name = .{ .short = 'c' } }, 619 | error.SomethingElse, 620 | "Error while parsing arguments: SomethingElse\n", 621 | ); 622 | try testDiag( 623 | .{ .name = .{ .long = "cc" } }, 624 | error.SomethingElse, 625 | "Error while parsing arguments: SomethingElse\n", 626 | ); 627 | } 628 | 629 | /// Options that can be set to customize the behavior of parsing. 630 | pub const ParseOptions = struct { 631 | allocator: std.mem.Allocator, 632 | diagnostic: ?*Diagnostic = null, 633 | 634 | /// The assignment separators, which by default is `=`. This is the separator between the name 635 | /// of an argument and its value. For `--arg=value`, `arg` is the name and `value` is the value 636 | /// if `=` is one of the assignment separators. 637 | assignment_separators: []const u8 = default_assignment_separators, 638 | 639 | /// This option makes `clap.parse` and `clap.parseEx` stop parsing after encountering a 640 | /// certain positional index. Setting `terminating_positional` to 0 will make them stop 641 | /// parsing after the 0th positional has been added to `positionals` (aka after parsing 1 642 | /// positional) 643 | terminating_positional: usize = std.math.maxInt(usize), 644 | }; 645 | 646 | /// Same as `parseEx` but uses the `args.OsIterator` by default. 647 | pub fn parse( 648 | comptime Id: type, 649 | comptime params: []const Param(Id), 650 | comptime value_parsers: anytype, 651 | opt: ParseOptions, 652 | ) !Result(Id, params, value_parsers) { 653 | var arena = std.heap.ArenaAllocator.init(opt.allocator); 654 | errdefer arena.deinit(); 655 | 656 | var iter = try std.process.ArgIterator.initWithAllocator(arena.allocator()); 657 | const exe_arg = iter.next(); 658 | 659 | const result = try parseEx(Id, params, value_parsers, &iter, .{ 660 | // Let's reuse the arena from the `ArgIterator` since we already have it. 661 | .allocator = arena.allocator(), 662 | .diagnostic = opt.diagnostic, 663 | .assignment_separators = opt.assignment_separators, 664 | .terminating_positional = opt.terminating_positional, 665 | }); 666 | 667 | return Result(Id, params, value_parsers){ 668 | .args = result.args, 669 | .positionals = result.positionals, 670 | .exe_arg = exe_arg, 671 | .arena = arena, 672 | }; 673 | } 674 | 675 | /// The result of `parse`. Is owned by the caller and should be freed with `deinit`. 676 | pub fn Result( 677 | comptime Id: type, 678 | comptime params: []const Param(Id), 679 | comptime value_parsers: anytype, 680 | ) type { 681 | return struct { 682 | args: Arguments(Id, params, value_parsers, .slice), 683 | positionals: Positionals(Id, params, value_parsers, .slice), 684 | exe_arg: ?[]const u8, 685 | arena: std.heap.ArenaAllocator, 686 | 687 | pub fn deinit(result: @This()) void { 688 | result.arena.deinit(); 689 | } 690 | }; 691 | } 692 | 693 | /// Parses the command line arguments passed into the program based on an array of parameters. 694 | /// 695 | /// The result will contain an `args` field which contains all the non positional arguments passed 696 | /// in. There is a field in `args` for each parameter. The name of that field will be the result 697 | /// of this expression: 698 | /// ``` 699 | /// param.names.longest().name` 700 | /// ``` 701 | /// 702 | /// The fields can have types other that `[]const u8` and this is based on what `value_parsers` 703 | /// you provide. The parser to use for each parameter is determined by the following expression: 704 | /// ``` 705 | /// @field(value_parsers, param.id.value()) 706 | /// ``` 707 | /// 708 | /// Where `value` is a function that returns the name of the value this parameter takes. A parser 709 | /// is simple a function with the signature: 710 | /// ``` 711 | /// fn ([]const u8) Error!T 712 | /// ``` 713 | /// 714 | /// `T` can be any type and `Error` can be any error. You can pass `clap.parsers.default` if you 715 | /// just wonna get something up and running. 716 | /// 717 | /// The result will also contain a `positionals` field which contains all positional arguments 718 | /// passed. This field will be a tuple with one field for each positional parameter. 719 | /// 720 | /// Example: 721 | /// -h, --help 722 | /// -s, --str 723 | /// -i, --int 724 | /// -m, --many ... 725 | /// 726 | /// ... 727 | /// 728 | /// struct { 729 | /// args: struct { 730 | /// help: u8, 731 | /// str: ?[]const u8, 732 | /// int: ?usize, 733 | /// many: []const usize, 734 | /// }, 735 | /// positionals: struct { 736 | /// ?u8, 737 | /// []const []const u8, 738 | /// }, 739 | /// } 740 | /// 741 | /// Caller owns the result and should free it by calling `result.deinit()` 742 | pub fn parseEx( 743 | comptime Id: type, 744 | comptime params: []const Param(Id), 745 | comptime value_parsers: anytype, 746 | iter: anytype, 747 | opt: ParseOptions, 748 | ) !ResultEx(Id, params, value_parsers) { 749 | const allocator = opt.allocator; 750 | 751 | var positional_count: usize = 0; 752 | var positionals = initPositionals(Id, params, value_parsers, .list); 753 | errdefer deinitPositionals(&positionals, allocator); 754 | 755 | var arguments = Arguments(Id, params, value_parsers, .list){}; 756 | errdefer deinitArgs(&arguments, allocator); 757 | 758 | var stream = streaming.Clap(Id, std.meta.Child(@TypeOf(iter))){ 759 | .params = params, 760 | .iter = iter, 761 | .diagnostic = opt.diagnostic, 762 | .assignment_separators = opt.assignment_separators, 763 | }; 764 | arg_loop: while (try stream.next()) |arg| { 765 | // This loop checks if we got a short or long parameter. If so, the value is parsed and 766 | // stored in `arguments` 767 | inline for (params) |*param| continue_params_loop: { 768 | const longest = comptime param.names.longest(); 769 | if (longest.kind == .positional) 770 | continue; 771 | 772 | if (param != arg.param) 773 | // This is a trick to emulate a runtime `continue` in an `inline for`. 774 | break :continue_params_loop; 775 | 776 | const parser = comptime switch (param.takes_value) { 777 | .none => null, 778 | .one, .many => @field(value_parsers, param.id.value()), 779 | }; 780 | 781 | const name = longest.name[0..longest.name.len].*; 782 | switch (param.takes_value) { 783 | .none => @field(arguments, &name) +|= 1, 784 | .one => @field(arguments, &name) = try parser(arg.value.?), 785 | .many => { 786 | const value = try parser(arg.value.?); 787 | try @field(arguments, &name).append(allocator, value); 788 | }, 789 | } 790 | } 791 | 792 | // This loop checks if we got a positional parameter. If so, the value is parsed and 793 | // stored in `positionals` 794 | comptime var positionals_index = 0; 795 | inline for (params) |*param| continue_params_loop: { 796 | const longest = comptime param.names.longest(); 797 | if (longest.kind != .positional) 798 | continue; 799 | 800 | const i = positionals_index; 801 | positionals_index += 1; 802 | 803 | if (arg.param.names.longest().kind != .positional) 804 | // This is a trick to emulate a runtime `continue` in an `inline for`. 805 | break :continue_params_loop; 806 | 807 | const parser = comptime switch (param.takes_value) { 808 | .none => null, 809 | .one, .many => @field(value_parsers, param.id.value()), 810 | }; 811 | 812 | // We keep track of how many positionals we have received. This is used to pick which 813 | // `positional` field to store to. Once `positional_count` exceeds the number of 814 | // positional parameters, the rest are stored in the last `positional` field. 815 | const pos = &positionals[i]; 816 | const last = positionals.len == i + 1; 817 | if ((last and positional_count >= i) or positional_count == i) { 818 | switch (@typeInfo(@TypeOf(pos.*))) { 819 | .optional => pos.* = try parser(arg.value.?), 820 | else => try pos.append(allocator, try parser(arg.value.?)), 821 | } 822 | 823 | if (opt.terminating_positional <= positional_count) 824 | break :arg_loop; 825 | positional_count += 1; 826 | continue :arg_loop; 827 | } 828 | } 829 | } 830 | 831 | // We are done parsing, but our arguments are stored in lists, and not slices. Map the list 832 | // fields to slices and return that. 833 | var result_args = Arguments(Id, params, value_parsers, .slice){}; 834 | inline for (std.meta.fields(@TypeOf(arguments))) |field| { 835 | switch (@typeInfo(field.type)) { 836 | .@"struct" => { 837 | const slice = try @field(arguments, field.name).toOwnedSlice(allocator); 838 | @field(result_args, field.name) = slice; 839 | }, 840 | else => @field(result_args, field.name) = @field(arguments, field.name), 841 | } 842 | } 843 | 844 | // We are done parsing, but our positionals are stored in lists, and not slices. 845 | var result_positionals: Positionals(Id, params, value_parsers, .slice) = undefined; 846 | inline for (&result_positionals, &positionals) |*res_pos, *pos| { 847 | switch (@typeInfo(@TypeOf(pos.*))) { 848 | .@"struct" => res_pos.* = try pos.toOwnedSlice(allocator), 849 | else => res_pos.* = pos.*, 850 | } 851 | } 852 | 853 | return ResultEx(Id, params, value_parsers){ 854 | .args = result_args, 855 | .positionals = result_positionals, 856 | .allocator = allocator, 857 | }; 858 | } 859 | 860 | /// The result of `parseEx`. Is owned by the caller and should be freed with `deinit`. 861 | pub fn ResultEx( 862 | comptime Id: type, 863 | comptime params: []const Param(Id), 864 | comptime value_parsers: anytype, 865 | ) type { 866 | return struct { 867 | args: Arguments(Id, params, value_parsers, .slice), 868 | positionals: Positionals(Id, params, value_parsers, .slice), 869 | allocator: std.mem.Allocator, 870 | 871 | pub fn deinit(result: *@This()) void { 872 | deinitArgs(&result.args, result.allocator); 873 | deinitPositionals(&result.positionals, result.allocator); 874 | } 875 | }; 876 | } 877 | 878 | /// Turn a list of parameters into a tuple with one field for each positional parameter. 879 | /// The type of each parameter field is determined by `ParamType`. 880 | fn Positionals( 881 | comptime Id: type, 882 | comptime params: []const Param(Id), 883 | comptime value_parsers: anytype, 884 | comptime multi_arg_kind: MultiArgKind, 885 | ) type { 886 | var fields_len: usize = 0; 887 | for (params) |param| { 888 | const longest = param.names.longest(); 889 | if (longest.kind != .positional) 890 | continue; 891 | fields_len += 1; 892 | } 893 | 894 | var fields: [fields_len]std.builtin.Type.StructField = undefined; 895 | var i: usize = 0; 896 | for (params) |param| { 897 | const longest = param.names.longest(); 898 | if (longest.kind != .positional) 899 | continue; 900 | 901 | const T = ParamType(Id, param, value_parsers); 902 | const FieldT = switch (param.takes_value) { 903 | .none => continue, 904 | .one => ?T, 905 | .many => switch (multi_arg_kind) { 906 | .slice => []const T, 907 | .list => std.ArrayListUnmanaged(T), 908 | }, 909 | }; 910 | 911 | fields[i] = .{ 912 | .name = std.fmt.comptimePrint("{}", .{i}), 913 | .type = FieldT, 914 | .default_value_ptr = null, 915 | .is_comptime = false, 916 | .alignment = @alignOf(FieldT), 917 | }; 918 | i += 1; 919 | } 920 | 921 | return @Type(.{ .@"struct" = .{ 922 | .layout = .auto, 923 | .fields = &fields, 924 | .decls = &.{}, 925 | .is_tuple = true, 926 | } }); 927 | } 928 | 929 | fn initPositionals( 930 | comptime Id: type, 931 | comptime params: []const Param(Id), 932 | comptime value_parsers: anytype, 933 | comptime multi_arg_kind: MultiArgKind, 934 | ) Positionals(Id, params, value_parsers, multi_arg_kind) { 935 | var res: Positionals(Id, params, value_parsers, multi_arg_kind) = undefined; 936 | 937 | comptime var i: usize = 0; 938 | inline for (params) |param| { 939 | const longest = comptime param.names.longest(); 940 | if (longest.kind != .positional) 941 | continue; 942 | 943 | const T = ParamType(Id, param, value_parsers); 944 | res[i] = switch (param.takes_value) { 945 | .none => continue, 946 | .one => @as(?T, null), 947 | .many => switch (multi_arg_kind) { 948 | .slice => @as([]const T, &[_]T{}), 949 | .list => std.ArrayListUnmanaged(T){}, 950 | }, 951 | }; 952 | i += 1; 953 | } 954 | 955 | return res; 956 | } 957 | 958 | /// Deinitializes a tuple of type `Positionals`. Since the `Positionals` type is generated, and we 959 | /// cannot add the deinit declaration to it, we declare it here instead. 960 | fn deinitPositionals(positionals: anytype, allocator: std.mem.Allocator) void { 961 | inline for (positionals) |*pos| { 962 | switch (@typeInfo(@TypeOf(pos.*))) { 963 | .optional => {}, 964 | .@"struct" => pos.deinit(allocator), 965 | else => allocator.free(pos.*), 966 | } 967 | } 968 | } 969 | 970 | /// Given a parameter figure out which type that parameter is parsed into when using the correct 971 | /// parser from `value_parsers`. 972 | fn ParamType(comptime Id: type, comptime param: Param(Id), comptime value_parsers: anytype) type { 973 | const parser = switch (param.takes_value) { 974 | .none => parsers.string, 975 | .one, .many => @field(value_parsers, param.id.value()), 976 | }; 977 | return parsers.Result(@TypeOf(parser)); 978 | } 979 | 980 | /// Deinitializes a struct of type `Argument`. Since the `Argument` type is generated, and we 981 | /// cannot add the deinit declaration to it, we declare it here instead. 982 | fn deinitArgs(arguments: anytype, allocator: std.mem.Allocator) void { 983 | inline for (@typeInfo(@TypeOf(arguments.*)).@"struct".fields) |field| { 984 | switch (@typeInfo(field.type)) { 985 | .int, .optional => {}, 986 | .@"struct" => @field(arguments, field.name).deinit(allocator), 987 | else => allocator.free(@field(arguments, field.name)), 988 | } 989 | } 990 | } 991 | 992 | const MultiArgKind = enum { slice, list }; 993 | 994 | /// Turn a list of parameters into a struct with one field for each none positional parameter. 995 | /// The type of each parameter field is determined by `ParamType`. Positional arguments will not 996 | /// have a field in this struct. 997 | fn Arguments( 998 | comptime Id: type, 999 | comptime params: []const Param(Id), 1000 | comptime value_parsers: anytype, 1001 | comptime multi_arg_kind: MultiArgKind, 1002 | ) type { 1003 | var fields_len: usize = 0; 1004 | for (params) |param| { 1005 | const longest = param.names.longest(); 1006 | if (longest.kind == .positional) 1007 | continue; 1008 | fields_len += 1; 1009 | } 1010 | 1011 | var fields: [fields_len]std.builtin.Type.StructField = undefined; 1012 | var i: usize = 0; 1013 | for (params) |param| { 1014 | const longest = param.names.longest(); 1015 | if (longest.kind == .positional) 1016 | continue; 1017 | 1018 | const T = ParamType(Id, param, value_parsers); 1019 | const default_value = switch (param.takes_value) { 1020 | .none => @as(u8, 0), 1021 | .one => @as(?T, null), 1022 | .many => switch (multi_arg_kind) { 1023 | .slice => @as([]const T, &[_]T{}), 1024 | .list => std.ArrayListUnmanaged(T){}, 1025 | }, 1026 | }; 1027 | 1028 | const name = longest.name[0..longest.name.len] ++ ""; // Adds null terminator 1029 | fields[i] = .{ 1030 | .name = name, 1031 | .type = @TypeOf(default_value), 1032 | .default_value_ptr = @ptrCast(&default_value), 1033 | .is_comptime = false, 1034 | .alignment = @alignOf(@TypeOf(default_value)), 1035 | }; 1036 | i += 1; 1037 | } 1038 | 1039 | return @Type(.{ .@"struct" = .{ 1040 | .layout = .auto, 1041 | .fields = &fields, 1042 | .decls = &.{}, 1043 | .is_tuple = false, 1044 | } }); 1045 | } 1046 | 1047 | test "str and u64" { 1048 | const params = comptime parseParamsComptime( 1049 | \\--str 1050 | \\--num 1051 | \\ 1052 | ); 1053 | 1054 | var iter = args.SliceIterator{ 1055 | .args = &.{ "--num", "10", "--str", "cooley_rec_inp_ptr" }, 1056 | }; 1057 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1058 | .allocator = std.testing.allocator, 1059 | }); 1060 | defer res.deinit(); 1061 | } 1062 | 1063 | test "different assignment separators" { 1064 | const params = comptime parseParamsComptime( 1065 | \\-a, --aa ... 1066 | \\ 1067 | ); 1068 | 1069 | var iter = args.SliceIterator{ 1070 | .args = &.{ "-a=0", "--aa=1", "-a:2", "--aa:3" }, 1071 | }; 1072 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1073 | .allocator = std.testing.allocator, 1074 | .assignment_separators = "=:", 1075 | }); 1076 | defer res.deinit(); 1077 | 1078 | try std.testing.expectEqualSlices(usize, &.{ 0, 1, 2, 3 }, res.args.aa); 1079 | } 1080 | 1081 | test "single positional" { 1082 | const params = comptime parseParamsComptime( 1083 | \\ 1084 | \\ 1085 | ); 1086 | 1087 | { 1088 | var iter = args.SliceIterator{ .args = &.{} }; 1089 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1090 | .allocator = std.testing.allocator, 1091 | }); 1092 | defer res.deinit(); 1093 | 1094 | try std.testing.expect(res.positionals[0] == null); 1095 | } 1096 | 1097 | { 1098 | var iter = args.SliceIterator{ .args = &.{"a"} }; 1099 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1100 | .allocator = std.testing.allocator, 1101 | }); 1102 | defer res.deinit(); 1103 | 1104 | try std.testing.expectEqualStrings("a", res.positionals[0].?); 1105 | } 1106 | 1107 | { 1108 | var iter = args.SliceIterator{ .args = &.{ "a", "b" } }; 1109 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1110 | .allocator = std.testing.allocator, 1111 | }); 1112 | defer res.deinit(); 1113 | 1114 | try std.testing.expectEqualStrings("b", res.positionals[0].?); 1115 | } 1116 | } 1117 | 1118 | test "multiple positionals" { 1119 | const params = comptime parseParamsComptime( 1120 | \\ 1121 | \\ 1122 | \\ 1123 | \\ 1124 | ); 1125 | 1126 | { 1127 | var iter = args.SliceIterator{ .args = &.{} }; 1128 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1129 | .allocator = std.testing.allocator, 1130 | }); 1131 | defer res.deinit(); 1132 | 1133 | try std.testing.expect(res.positionals[0] == null); 1134 | try std.testing.expect(res.positionals[1] == null); 1135 | try std.testing.expect(res.positionals[2] == null); 1136 | } 1137 | 1138 | { 1139 | var iter = args.SliceIterator{ .args = &.{"1"} }; 1140 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1141 | .allocator = std.testing.allocator, 1142 | }); 1143 | defer res.deinit(); 1144 | 1145 | try std.testing.expectEqual(@as(u8, 1), res.positionals[0].?); 1146 | try std.testing.expect(res.positionals[1] == null); 1147 | try std.testing.expect(res.positionals[2] == null); 1148 | } 1149 | 1150 | { 1151 | var iter = args.SliceIterator{ .args = &.{ "1", "2" } }; 1152 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1153 | .allocator = std.testing.allocator, 1154 | }); 1155 | defer res.deinit(); 1156 | 1157 | try std.testing.expectEqual(@as(u8, 1), res.positionals[0].?); 1158 | try std.testing.expectEqual(@as(u8, 2), res.positionals[1].?); 1159 | try std.testing.expect(res.positionals[2] == null); 1160 | } 1161 | 1162 | { 1163 | var iter = args.SliceIterator{ .args = &.{ "1", "2", "b" } }; 1164 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1165 | .allocator = std.testing.allocator, 1166 | }); 1167 | defer res.deinit(); 1168 | 1169 | try std.testing.expectEqual(@as(u8, 1), res.positionals[0].?); 1170 | try std.testing.expectEqual(@as(u8, 2), res.positionals[1].?); 1171 | try std.testing.expectEqualStrings("b", res.positionals[2].?); 1172 | } 1173 | } 1174 | 1175 | test "everything" { 1176 | const params = comptime parseParamsComptime( 1177 | \\-a, --aa 1178 | \\-b, --bb 1179 | \\-c, --cc 1180 | \\-d, --dd ... 1181 | \\-h 1182 | \\... 1183 | \\ 1184 | ); 1185 | 1186 | var iter = args.SliceIterator{ 1187 | .args = &.{ "-a", "--aa", "-c", "0", "something", "-d", "1", "--dd", "2", "-h" }, 1188 | }; 1189 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1190 | .allocator = std.testing.allocator, 1191 | }); 1192 | defer res.deinit(); 1193 | 1194 | try std.testing.expect(res.args.aa == 2); 1195 | try std.testing.expect(res.args.bb == 0); 1196 | try std.testing.expect(res.args.h == 1); 1197 | try std.testing.expectEqualStrings("0", res.args.cc.?); 1198 | try std.testing.expectEqual(@as(usize, 1), res.positionals.len); 1199 | try std.testing.expectEqualStrings("something", res.positionals[0][0]); 1200 | try std.testing.expectEqualSlices(usize, &.{ 1, 2 }, res.args.dd); 1201 | try std.testing.expectEqual(@as(usize, 10), iter.index); 1202 | } 1203 | 1204 | test "terminating positional" { 1205 | const params = comptime parseParamsComptime( 1206 | \\-a, --aa 1207 | \\-b, --bb 1208 | \\-c, --cc 1209 | \\-d, --dd ... 1210 | \\-h 1211 | \\... 1212 | \\ 1213 | ); 1214 | 1215 | var iter = args.SliceIterator{ 1216 | .args = &.{ "-a", "--aa", "-c", "0", "something", "-d", "1", "--dd", "2", "-h" }, 1217 | }; 1218 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1219 | .allocator = std.testing.allocator, 1220 | .terminating_positional = 0, 1221 | }); 1222 | defer res.deinit(); 1223 | 1224 | try std.testing.expect(res.args.aa == 2); 1225 | try std.testing.expect(res.args.bb == 0); 1226 | try std.testing.expect(res.args.h == 0); 1227 | try std.testing.expectEqualStrings("0", res.args.cc.?); 1228 | try std.testing.expectEqual(@as(usize, 1), res.positionals.len); 1229 | try std.testing.expectEqual(@as(usize, 1), res.positionals[0].len); 1230 | try std.testing.expectEqualStrings("something", res.positionals[0][0]); 1231 | try std.testing.expectEqualSlices(usize, &.{}, res.args.dd); 1232 | try std.testing.expectEqual(@as(usize, 5), iter.index); 1233 | } 1234 | 1235 | test "overflow-safe" { 1236 | const params = comptime parseParamsComptime( 1237 | \\-a, --aa 1238 | ); 1239 | 1240 | var iter = args.SliceIterator{ 1241 | .args = &(.{"-" ++ ("a" ** 300)}), 1242 | }; 1243 | 1244 | // This just needs to not crash 1245 | var res = try parseEx(Help, ¶ms, parsers.default, &iter, .{ 1246 | .allocator = std.testing.allocator, 1247 | }); 1248 | defer res.deinit(); 1249 | } 1250 | 1251 | test "empty" { 1252 | var iter = args.SliceIterator{ .args = &.{} }; 1253 | var res = try parseEx(u8, &[_]Param(u8){}, parsers.default, &iter, .{ 1254 | .allocator = std.testing.allocator, 1255 | }); 1256 | defer res.deinit(); 1257 | } 1258 | 1259 | fn testErr( 1260 | comptime params: []const Param(Help), 1261 | args_strings: []const []const u8, 1262 | expected: []const u8, 1263 | ) !void { 1264 | var diag = Diagnostic{}; 1265 | var iter = args.SliceIterator{ .args = args_strings }; 1266 | _ = parseEx(Help, params, parsers.default, &iter, .{ 1267 | .allocator = std.testing.allocator, 1268 | .diagnostic = &diag, 1269 | }) catch |err| { 1270 | var buf: [1024]u8 = undefined; 1271 | var writer = std.Io.Writer.fixed(&buf); 1272 | try diag.report(&writer, err); 1273 | try std.testing.expectEqualStrings(expected, writer.buffered()); 1274 | return; 1275 | }; 1276 | 1277 | try std.testing.expect(false); 1278 | } 1279 | 1280 | test "errors" { 1281 | const params = comptime parseParamsComptime( 1282 | \\-a, --aa 1283 | \\-c, --cc 1284 | \\ 1285 | ); 1286 | 1287 | try testErr(¶ms, &.{"q"}, "Invalid argument 'q'\n"); 1288 | try testErr(¶ms, &.{"-q"}, "Invalid argument '-q'\n"); 1289 | try testErr(¶ms, &.{"--q"}, "Invalid argument '--q'\n"); 1290 | try testErr(¶ms, &.{"--q=1"}, "Invalid argument '--q'\n"); 1291 | try testErr(¶ms, &.{"-a=1"}, "The argument '-a' does not take a value\n"); 1292 | try testErr(¶ms, &.{"--aa=1"}, "The argument '--aa' does not take a value\n"); 1293 | try testErr(¶ms, &.{"-c"}, "The argument '-c' requires a value but none was supplied\n"); 1294 | try testErr( 1295 | ¶ms, 1296 | &.{"--cc"}, 1297 | "The argument '--cc' requires a value but none was supplied\n", 1298 | ); 1299 | } 1300 | 1301 | pub const Help = struct { 1302 | desc: []const u8 = "", 1303 | val: []const u8 = "", 1304 | 1305 | pub fn description(h: Help) []const u8 { 1306 | return h.desc; 1307 | } 1308 | 1309 | pub fn value(h: Help) []const u8 { 1310 | return h.val; 1311 | } 1312 | }; 1313 | 1314 | pub const HelpOptions = struct { 1315 | /// Render the description of a parameter in a similar way to how markdown would render 1316 | /// such a string. This means that single newlines won't be respected unless followed by 1317 | /// bullet points or other markdown elements. 1318 | markdown_lite: bool = true, 1319 | 1320 | /// Whether `help` should print the description of a parameter on a new line instead of after 1321 | /// the parameter names. This options works together with `description_indent` to change 1322 | /// where descriptions are printed. 1323 | /// 1324 | /// description_on_new_line=false, description_indent=4 1325 | /// 1326 | /// -a, --aa This is a description 1327 | /// that is not placed on 1328 | /// a new line. 1329 | /// 1330 | /// description_on_new_line=true, description_indent=4 1331 | /// 1332 | /// -a, --aa 1333 | /// This is a description 1334 | /// that is placed on a 1335 | /// new line. 1336 | description_on_new_line: bool = true, 1337 | 1338 | /// How much to indent descriptions. See `description_on_new_line` for examples of how this 1339 | /// changes the output. 1340 | description_indent: usize = 8, 1341 | 1342 | /// How much to indent each parameter. 1343 | /// 1344 | /// indent=0, description_on_new_line=false, description_indent=4 1345 | /// 1346 | /// -a, --aa This is a description 1347 | /// that is not placed on 1348 | /// a new line. 1349 | /// 1350 | /// indent=4, description_on_new_line=false, description_indent=4 1351 | /// 1352 | /// -a, --aa This is a description 1353 | /// that is not placed on 1354 | /// a new line. 1355 | /// 1356 | indent: usize = 4, 1357 | 1358 | /// The maximum width of the help message. `help` will try to break the description of 1359 | /// parameters into multiple lines if they exceed this maximum. Setting this to the width 1360 | /// of the terminal is a nice way of using this option. 1361 | max_width: usize = std.math.maxInt(usize), 1362 | 1363 | /// The number of empty lines between each printed parameter. 1364 | spacing_between_parameters: usize = 1, 1365 | }; 1366 | 1367 | /// Wrapper around `help`, which writes to a file in a buffered manner 1368 | pub fn helpToFile( 1369 | file: std.fs.File, 1370 | comptime Id: type, 1371 | params: []const Param(Id), 1372 | opt: HelpOptions, 1373 | ) !void { 1374 | var buf: [1024]u8 = undefined; 1375 | var writer = file.writer(&buf); 1376 | try help(&writer.interface, Id, params, opt); 1377 | return writer.interface.flush(); 1378 | } 1379 | 1380 | /// Print a slice of `Param` formatted as a help string to `writer`. This function expects 1381 | /// `Id` to have the methods `description` and `value` which are used by `help` to describe 1382 | /// each parameter. Using `Help` as `Id` is good choice. 1383 | /// 1384 | /// The output can be constumized with the `opt` parameter. For default formatting `.{}` can 1385 | /// be passed. 1386 | pub fn help( 1387 | writer: *std.Io.Writer, 1388 | comptime Id: type, 1389 | params: []const Param(Id), 1390 | opt: HelpOptions, 1391 | ) !void { 1392 | const max_spacing = blk: { 1393 | var res: usize = 0; 1394 | for (params) |param| { 1395 | var discarding = std.Io.Writer.Discarding.init(&.{}); 1396 | var cs = ccw.CodepointCountingWriter.init(&discarding.writer); 1397 | try printParam(&cs.interface, Id, param); 1398 | if (res < cs.codepoints_written) 1399 | res = @intCast(cs.codepoints_written); 1400 | } 1401 | 1402 | break :blk res; 1403 | }; 1404 | 1405 | const description_indentation = opt.indent + 1406 | opt.description_indent + 1407 | max_spacing * @intFromBool(!opt.description_on_new_line); 1408 | 1409 | var first_parameter: bool = true; 1410 | for (params) |param| { 1411 | if (!first_parameter) 1412 | try writer.splatByteAll('\n', opt.spacing_between_parameters); 1413 | 1414 | first_parameter = false; 1415 | try writer.splatByteAll(' ', opt.indent); 1416 | 1417 | var cw = ccw.CodepointCountingWriter.init(writer); 1418 | try printParam(&cw.interface, Id, param); 1419 | 1420 | var description_writer = DescriptionWriter{ 1421 | .underlying_writer = writer, 1422 | .indentation = description_indentation, 1423 | .printed_chars = @intCast(cw.codepoints_written), 1424 | .max_width = opt.max_width, 1425 | }; 1426 | 1427 | if (opt.description_on_new_line) 1428 | try description_writer.newline(); 1429 | 1430 | const min_description_indent = blk: { 1431 | const description = param.id.description(); 1432 | 1433 | var first_line = true; 1434 | var res: usize = std.math.maxInt(usize); 1435 | var it = std.mem.tokenizeScalar(u8, description, '\n'); 1436 | while (it.next()) |line| : (first_line = false) { 1437 | const trimmed = std.mem.trimLeft(u8, line, " "); 1438 | const indent = line.len - trimmed.len; 1439 | 1440 | // If the first line has no indentation, then we ignore the indentation of the 1441 | // first line. We do this as the parameter might have been parsed from: 1442 | // 1443 | // -a, --aa The first line 1444 | // is not indented, 1445 | // but the rest of 1446 | // the lines are. 1447 | // 1448 | // In this case, we want to pretend that the first line has the same indentation 1449 | // as the min_description_indent, even though it is not so in the string we get. 1450 | if (first_line and indent == 0) 1451 | continue; 1452 | if (indent < res) 1453 | res = indent; 1454 | } 1455 | 1456 | break :blk res; 1457 | }; 1458 | 1459 | const description = param.id.description(); 1460 | var it = std.mem.splitScalar(u8, description, '\n'); 1461 | var first_line = true; 1462 | var non_emitted_newlines: usize = 0; 1463 | var last_line_indentation: usize = 0; 1464 | while (it.next()) |raw_line| : (first_line = false) { 1465 | // First line might be special. See comment above. 1466 | const indented_line = if (first_line and !std.mem.startsWith(u8, raw_line, " ")) 1467 | raw_line 1468 | else 1469 | raw_line[@min(min_description_indent, raw_line.len)..]; 1470 | 1471 | const line = std.mem.trimLeft(u8, indented_line, " "); 1472 | if (line.len == 0) { 1473 | non_emitted_newlines += 1; 1474 | continue; 1475 | } 1476 | 1477 | const line_indentation = indented_line.len - line.len; 1478 | description_writer.indentation = description_indentation + line_indentation; 1479 | 1480 | if (opt.markdown_lite) { 1481 | const new_paragraph = non_emitted_newlines > 1; 1482 | 1483 | const does_not_have_same_indent_as_last_line = 1484 | line_indentation != last_line_indentation; 1485 | 1486 | const starts_with_control_char = std.mem.indexOfScalar(u8, "=*", line[0]) != null; 1487 | 1488 | // Either the input contains 2 or more newlines, in which case we should start 1489 | // a new paragraph. 1490 | if (new_paragraph) { 1491 | try description_writer.newline(); 1492 | try description_writer.newline(); 1493 | } 1494 | // Or this line has a special control char or different indentation which means 1495 | // we should output it on a new line as well. 1496 | else if (starts_with_control_char or does_not_have_same_indent_as_last_line) { 1497 | try description_writer.newline(); 1498 | } 1499 | } else { 1500 | // For none markdown like format, we just respect the newlines in the input 1501 | // string and output them as is. 1502 | for (0..non_emitted_newlines) |_| 1503 | try description_writer.newline(); 1504 | } 1505 | 1506 | var words = std.mem.tokenizeScalar(u8, line, ' '); 1507 | while (words.next()) |word| 1508 | try description_writer.writeWord(word); 1509 | 1510 | // We have not emitted the end of this line yet. 1511 | non_emitted_newlines = 1; 1512 | last_line_indentation = line_indentation; 1513 | } 1514 | 1515 | try writer.writeAll("\n"); 1516 | } 1517 | } 1518 | 1519 | const DescriptionWriter = struct { 1520 | underlying_writer: *std.Io.Writer, 1521 | 1522 | indentation: usize, 1523 | max_width: usize, 1524 | printed_chars: usize, 1525 | 1526 | pub fn writeWord(writer: *@This(), word: []const u8) !void { 1527 | std.debug.assert(word.len != 0); 1528 | 1529 | var first_word = writer.printed_chars <= writer.indentation; 1530 | const chars_to_write = try std.unicode.utf8CountCodepoints(word) + @intFromBool(!first_word); 1531 | if (chars_to_write + writer.printed_chars > writer.max_width) { 1532 | // If the word does not fit on this line, then we insert a new line and print 1533 | // it on that line. The only exception to this is if this was the first word. 1534 | // If the first word does not fit on this line, then it will also not fit on the 1535 | // next one. In that case, all we can really do is just output the word. 1536 | if (!first_word) 1537 | try writer.newline(); 1538 | 1539 | first_word = true; 1540 | } 1541 | 1542 | if (!first_word) 1543 | try writer.underlying_writer.writeAll(" "); 1544 | 1545 | try writer.ensureIndented(); 1546 | try writer.underlying_writer.writeAll(word); 1547 | writer.printed_chars += chars_to_write; 1548 | } 1549 | 1550 | pub fn newline(writer: *@This()) !void { 1551 | try writer.underlying_writer.writeAll("\n"); 1552 | writer.printed_chars = 0; 1553 | } 1554 | 1555 | fn ensureIndented(writer: *@This()) !void { 1556 | if (writer.printed_chars < writer.indentation) { 1557 | const to_indent = writer.indentation - writer.printed_chars; 1558 | try writer.underlying_writer.splatByteAll(' ', to_indent); 1559 | writer.printed_chars += to_indent; 1560 | } 1561 | } 1562 | }; 1563 | 1564 | fn printParam( 1565 | stream: *std.Io.Writer, 1566 | comptime Id: type, 1567 | param: Param(Id), 1568 | ) !void { 1569 | if (param.names.short != null or param.names.long != null) { 1570 | try stream.writeAll(&[_]u8{ 1571 | if (param.names.short) |_| '-' else ' ', 1572 | param.names.short orelse ' ', 1573 | }); 1574 | 1575 | if (param.names.long) |l| { 1576 | try stream.writeByte(if (param.names.short) |_| ',' else ' '); 1577 | try stream.writeAll(" --"); 1578 | try stream.writeAll(l); 1579 | } 1580 | 1581 | if (param.takes_value != .none) 1582 | try stream.writeAll(" "); 1583 | } 1584 | 1585 | if (param.takes_value == .none) 1586 | return; 1587 | 1588 | try stream.writeAll("<"); 1589 | try stream.writeAll(param.id.value()); 1590 | try stream.writeAll(">"); 1591 | if (param.takes_value == .many) 1592 | try stream.writeAll("..."); 1593 | } 1594 | 1595 | fn testHelp(opt: HelpOptions, str: []const u8) !void { 1596 | const params = try parseParams(std.testing.allocator, str); 1597 | defer std.testing.allocator.free(params); 1598 | 1599 | var buf: [2048]u8 = undefined; 1600 | var writer = std.Io.Writer.fixed(&buf); 1601 | try help(&writer, Help, params, opt); 1602 | try std.testing.expectEqualStrings(str, writer.buffered()); 1603 | } 1604 | 1605 | test "clap.help" { 1606 | try testHelp(.{}, 1607 | \\ -a 1608 | \\ Short flag. 1609 | \\ 1610 | \\ -b 1611 | \\ Short option. 1612 | \\ 1613 | \\ --aa 1614 | \\ Long flag. 1615 | \\ 1616 | \\ --bb 1617 | \\ Long option. 1618 | \\ 1619 | \\ -c, --cc 1620 | \\ Both flag. 1621 | \\ 1622 | \\ --complicate 1623 | \\ Flag with a complicated and very long description that spans multiple lines. 1624 | \\ 1625 | \\ Paragraph number 2: 1626 | \\ * Bullet point 1627 | \\ * Bullet point 1628 | \\ 1629 | \\ Example: 1630 | \\ something something something 1631 | \\ 1632 | \\ -d, --dd 1633 | \\ Both option. 1634 | \\ 1635 | \\ -d, --dd ... 1636 | \\ Both repeated option. 1637 | \\ 1638 | \\ 1639 | \\ Help text 1640 | \\ 1641 | \\ ... 1642 | \\ Another help text 1643 | \\ 1644 | ); 1645 | 1646 | try testHelp(.{ .markdown_lite = false }, 1647 | \\ -a 1648 | \\ Short flag. 1649 | \\ 1650 | \\ -b 1651 | \\ Short option. 1652 | \\ 1653 | \\ --aa 1654 | \\ Long flag. 1655 | \\ 1656 | \\ --bb 1657 | \\ Long option. 1658 | \\ 1659 | \\ -c, --cc 1660 | \\ Both flag. 1661 | \\ 1662 | \\ --complicate 1663 | \\ Flag with a complicated and 1664 | \\ very long description that 1665 | \\ spans multiple lines. 1666 | \\ 1667 | \\ Paragraph number 2: 1668 | \\ * Bullet point 1669 | \\ * Bullet point 1670 | \\ 1671 | \\ 1672 | \\ Example: 1673 | \\ something something something 1674 | \\ 1675 | \\ -d, --dd 1676 | \\ Both option. 1677 | \\ 1678 | \\ -d, --dd ... 1679 | \\ Both repeated option. 1680 | \\ 1681 | ); 1682 | 1683 | try testHelp(.{ .indent = 0 }, 1684 | \\-a 1685 | \\ Short flag. 1686 | \\ 1687 | \\-b 1688 | \\ Short option. 1689 | \\ 1690 | \\ --aa 1691 | \\ Long flag. 1692 | \\ 1693 | \\ --bb 1694 | \\ Long option. 1695 | \\ 1696 | \\-c, --cc 1697 | \\ Both flag. 1698 | \\ 1699 | \\ --complicate 1700 | \\ Flag with a complicated and very long description that spans multiple lines. 1701 | \\ 1702 | \\ Paragraph number 2: 1703 | \\ * Bullet point 1704 | \\ * Bullet point 1705 | \\ 1706 | \\ Example: 1707 | \\ something something something 1708 | \\ 1709 | \\-d, --dd 1710 | \\ Both option. 1711 | \\ 1712 | \\-d, --dd ... 1713 | \\ Both repeated option. 1714 | \\ 1715 | ); 1716 | 1717 | try testHelp(.{ .indent = 0 }, 1718 | \\-a 1719 | \\ Short flag. 1720 | \\ 1721 | \\-b 1722 | \\ Short option. 1723 | \\ 1724 | \\ --aa 1725 | \\ Long flag. 1726 | \\ 1727 | \\ --bb 1728 | \\ Long option. 1729 | \\ 1730 | \\-c, --cc 1731 | \\ Both flag. 1732 | \\ 1733 | \\ --complicate 1734 | \\ Flag with a complicated and very long description that spans multiple lines. 1735 | \\ 1736 | \\ Paragraph number 2: 1737 | \\ * Bullet point 1738 | \\ * Bullet point 1739 | \\ 1740 | \\ Example: 1741 | \\ something something something 1742 | \\ 1743 | \\-d, --dd 1744 | \\ Both option. 1745 | \\ 1746 | \\-d, --dd ... 1747 | \\ Both repeated option. 1748 | \\ 1749 | ); 1750 | 1751 | try testHelp(.{ .indent = 0, .max_width = 26 }, 1752 | \\-a 1753 | \\ Short flag. 1754 | \\ 1755 | \\-b 1756 | \\ Short option. 1757 | \\ 1758 | \\ --aa 1759 | \\ Long flag. 1760 | \\ 1761 | \\ --bb 1762 | \\ Long option. 1763 | \\ 1764 | \\-c, --cc 1765 | \\ Both flag. 1766 | \\ 1767 | \\ --complicate 1768 | \\ Flag with a 1769 | \\ complicated and 1770 | \\ very long 1771 | \\ description that 1772 | \\ spans multiple 1773 | \\ lines. 1774 | \\ 1775 | \\ Paragraph number 1776 | \\ 2: 1777 | \\ * Bullet point 1778 | \\ * Bullet point 1779 | \\ 1780 | \\ Example: 1781 | \\ something 1782 | \\ something 1783 | \\ something 1784 | \\ 1785 | \\-d, --dd 1786 | \\ Both option. 1787 | \\ 1788 | \\-d, --dd ... 1789 | \\ Both repeated 1790 | \\ option. 1791 | \\ 1792 | ); 1793 | 1794 | try testHelp(.{ 1795 | .indent = 0, 1796 | .max_width = 26, 1797 | .description_indent = 6, 1798 | }, 1799 | \\-a 1800 | \\ Short flag. 1801 | \\ 1802 | \\-b 1803 | \\ Short option. 1804 | \\ 1805 | \\ --aa 1806 | \\ Long flag. 1807 | \\ 1808 | \\ --bb 1809 | \\ Long option. 1810 | \\ 1811 | \\-c, --cc 1812 | \\ Both flag. 1813 | \\ 1814 | \\ --complicate 1815 | \\ Flag with a 1816 | \\ complicated and 1817 | \\ very long 1818 | \\ description that 1819 | \\ spans multiple 1820 | \\ lines. 1821 | \\ 1822 | \\ Paragraph number 2: 1823 | \\ * Bullet point 1824 | \\ * Bullet point 1825 | \\ 1826 | \\ Example: 1827 | \\ something 1828 | \\ something 1829 | \\ something 1830 | \\ 1831 | \\-d, --dd 1832 | \\ Both option. 1833 | \\ 1834 | \\-d, --dd ... 1835 | \\ Both repeated 1836 | \\ option. 1837 | \\ 1838 | ); 1839 | 1840 | try testHelp(.{ 1841 | .indent = 0, 1842 | .max_width = 46, 1843 | .description_on_new_line = false, 1844 | }, 1845 | \\-a Short flag. 1846 | \\ 1847 | \\-b Short option. 1848 | \\ 1849 | \\ --aa Long flag. 1850 | \\ 1851 | \\ --bb Long option. 1852 | \\ 1853 | \\-c, --cc Both flag. 1854 | \\ 1855 | \\ --complicate Flag with a 1856 | \\ complicated and very 1857 | \\ long description that 1858 | \\ spans multiple lines. 1859 | \\ 1860 | \\ Paragraph number 2: 1861 | \\ * Bullet point 1862 | \\ * Bullet point 1863 | \\ 1864 | \\ Example: 1865 | \\ something 1866 | \\ something 1867 | \\ something 1868 | \\ 1869 | \\-d, --dd Both option. 1870 | \\ 1871 | \\-d, --dd ... Both repeated option. 1872 | \\ 1873 | ); 1874 | 1875 | try testHelp(.{ 1876 | .indent = 0, 1877 | .max_width = 46, 1878 | .description_on_new_line = false, 1879 | .description_indent = 4, 1880 | }, 1881 | \\-a Short flag. 1882 | \\ 1883 | \\-b Short option. 1884 | \\ 1885 | \\ --aa Long flag. 1886 | \\ 1887 | \\ --bb Long option. 1888 | \\ 1889 | \\-c, --cc Both flag. 1890 | \\ 1891 | \\ --complicate Flag with a complicated 1892 | \\ and very long description 1893 | \\ that spans multiple 1894 | \\ lines. 1895 | \\ 1896 | \\ Paragraph number 2: 1897 | \\ * Bullet point 1898 | \\ * Bullet point 1899 | \\ 1900 | \\ Example: 1901 | \\ something something 1902 | \\ something 1903 | \\ 1904 | \\-d, --dd Both option. 1905 | \\ 1906 | \\-d, --dd ... Both repeated option. 1907 | \\ 1908 | ); 1909 | 1910 | try testHelp(.{ 1911 | .indent = 0, 1912 | .max_width = 46, 1913 | .description_on_new_line = false, 1914 | .description_indent = 4, 1915 | .spacing_between_parameters = 0, 1916 | }, 1917 | \\-a Short flag. 1918 | \\-b Short option. 1919 | \\ --aa Long flag. 1920 | \\ --bb Long option. 1921 | \\-c, --cc Both flag. 1922 | \\ --complicate Flag with a complicated 1923 | \\ and very long description 1924 | \\ that spans multiple 1925 | \\ lines. 1926 | \\ 1927 | \\ Paragraph number 2: 1928 | \\ * Bullet point 1929 | \\ * Bullet point 1930 | \\ 1931 | \\ Example: 1932 | \\ something something 1933 | \\ something 1934 | \\-d, --dd Both option. 1935 | \\-d, --dd ... Both repeated option. 1936 | \\ 1937 | ); 1938 | 1939 | try testHelp(.{ 1940 | .indent = 0, 1941 | .max_width = 46, 1942 | .description_on_new_line = false, 1943 | .description_indent = 4, 1944 | .spacing_between_parameters = 2, 1945 | }, 1946 | \\-a Short flag. 1947 | \\ 1948 | \\ 1949 | \\-b Short option. 1950 | \\ 1951 | \\ 1952 | \\ --aa Long flag. 1953 | \\ 1954 | \\ 1955 | \\ --bb Long option. 1956 | \\ 1957 | \\ 1958 | \\-c, --cc Both flag. 1959 | \\ 1960 | \\ 1961 | \\ --complicate Flag with a complicated 1962 | \\ and very long description 1963 | \\ that spans multiple 1964 | \\ lines. 1965 | \\ 1966 | \\ Paragraph number 2: 1967 | \\ * Bullet point 1968 | \\ * Bullet point 1969 | \\ 1970 | \\ Example: 1971 | \\ something something 1972 | \\ something 1973 | \\ 1974 | \\ 1975 | \\-d, --dd Both option. 1976 | \\ 1977 | \\ 1978 | \\-d, --dd ... Both repeated option. 1979 | \\ 1980 | ); 1981 | 1982 | // Test with multibyte characters. 1983 | try testHelp(.{ 1984 | .indent = 0, 1985 | .max_width = 46, 1986 | .description_on_new_line = false, 1987 | .description_indent = 4, 1988 | .spacing_between_parameters = 2, 1989 | }, 1990 | \\-a Shört flåg. 1991 | \\ 1992 | \\ 1993 | \\-b Shört öptiön. 1994 | \\ 1995 | \\ 1996 | \\ --aa Löng fläg. 1997 | \\ 1998 | \\ 1999 | \\ --bb Löng öptiön. 2000 | \\ 2001 | \\ 2002 | \\-c, --cc Bóth fläg. 2003 | \\ 2004 | \\ 2005 | \\ --complicate Fläg wíth ä cömplǐcätéd 2006 | \\ änd vërý löng dèscrıptıön 2007 | \\ thät späns mültíplë 2008 | \\ lınēs. 2009 | \\ 2010 | \\ Pärägräph number 2: 2011 | \\ * Bullet pöint 2012 | \\ * Bullet pöint 2013 | \\ 2014 | \\ Exämple: 2015 | \\ sömething sömething 2016 | \\ sömething 2017 | \\ 2018 | \\ 2019 | \\-d, --dd Böth öptiön. 2020 | \\ 2021 | \\ 2022 | \\-d, --dd ... Böth repeäted öptiön. 2023 | \\ 2024 | ); 2025 | } 2026 | 2027 | /// Wrapper around `usage`, which writes to a file in a buffered manner 2028 | pub fn usageToFile(file: std.fs.File, comptime Id: type, params: []const Param(Id)) !void { 2029 | var buf: [1024]u8 = undefined; 2030 | var writer = file.writer(&buf); 2031 | try usage(&writer.interface, Id, params); 2032 | return writer.interface.flush(); 2033 | } 2034 | 2035 | /// Will print a usage message in the following format: 2036 | /// [-abc] [--longa] [-d ] [--longb ] 2037 | /// 2038 | /// First all none value taking parameters, which have a short name are printed, then non 2039 | /// positional parameters and finally the positional. 2040 | pub fn usage(stream: *std.Io.Writer, comptime Id: type, params: []const Param(Id)) !void { 2041 | var cos = ccw.CodepointCountingWriter.init(stream); 2042 | const cs = &cos.interface; 2043 | for (params) |param| { 2044 | const name = param.names.short orelse continue; 2045 | if (param.takes_value != .none) 2046 | continue; 2047 | 2048 | if (cos.codepoints_written == 0) 2049 | try stream.writeAll("[-"); 2050 | try cs.writeByte(name); 2051 | } 2052 | if (cos.codepoints_written != 0) 2053 | try cs.writeAll("]"); 2054 | 2055 | var has_positionals: bool = false; 2056 | for (params) |param| { 2057 | if (param.takes_value == .none and param.names.short != null) 2058 | continue; 2059 | 2060 | const prefix = if (param.names.short) |_| "-" else "--"; 2061 | const name = blk: { 2062 | if (param.names.short) |*s| 2063 | break :blk @as(*const [1]u8, s); 2064 | if (param.names.long) |l| 2065 | break :blk l; 2066 | 2067 | has_positionals = true; 2068 | continue; 2069 | }; 2070 | 2071 | if (cos.codepoints_written != 0) 2072 | try cs.writeAll(" "); 2073 | 2074 | try cs.writeAll("["); 2075 | try cs.writeAll(prefix); 2076 | try cs.writeAll(name); 2077 | if (param.takes_value != .none) { 2078 | try cs.writeAll(" <"); 2079 | try cs.writeAll(param.id.value()); 2080 | try cs.writeAll(">"); 2081 | if (param.takes_value == .many) 2082 | try cs.writeAll("..."); 2083 | } 2084 | 2085 | try cs.writeAll("]"); 2086 | } 2087 | 2088 | if (!has_positionals) 2089 | return; 2090 | 2091 | for (params) |param| { 2092 | if (param.names.short != null or param.names.long != null) 2093 | continue; 2094 | 2095 | if (cos.codepoints_written != 0) 2096 | try cs.writeAll(" "); 2097 | 2098 | try cs.writeAll("<"); 2099 | try cs.writeAll(param.id.value()); 2100 | try cs.writeAll(">"); 2101 | if (param.takes_value == .many) 2102 | try cs.writeAll("..."); 2103 | } 2104 | } 2105 | 2106 | fn testUsage(expected: []const u8, params: []const Param(Help)) !void { 2107 | var buf: [1024]u8 = undefined; 2108 | var writer = std.Io.Writer.fixed(&buf); 2109 | try usage(&writer, Help, params); 2110 | try std.testing.expectEqualStrings(expected, writer.buffered()); 2111 | } 2112 | 2113 | test "usage" { 2114 | @setEvalBranchQuota(100000); 2115 | try testUsage("[-ab]", &comptime parseParamsComptime( 2116 | \\-a 2117 | \\-b 2118 | \\ 2119 | )); 2120 | try testUsage("[-a ] [-b ]", &comptime parseParamsComptime( 2121 | \\-a 2122 | \\-b 2123 | \\ 2124 | )); 2125 | try testUsage("[--a] [--b]", &comptime parseParamsComptime( 2126 | \\--a 2127 | \\--b 2128 | \\ 2129 | )); 2130 | try testUsage("[--a ] [--b ]", &comptime parseParamsComptime( 2131 | \\--a 2132 | \\--b 2133 | \\ 2134 | )); 2135 | try testUsage("", &comptime parseParamsComptime( 2136 | \\ 2137 | \\ 2138 | )); 2139 | try testUsage("...", &comptime parseParamsComptime( 2140 | \\... 2141 | \\ 2142 | )); 2143 | try testUsage( 2144 | "[-ab] [-c ] [-d ] [--e] [--f] [--g ] [--h ] [-i ...] ", 2145 | &comptime parseParamsComptime( 2146 | \\-a 2147 | \\-b 2148 | \\-c 2149 | \\-d 2150 | \\--e 2151 | \\--f 2152 | \\--g 2153 | \\--h 2154 | \\-i ... 2155 | \\ 2156 | \\ 2157 | ), 2158 | ); 2159 | try testUsage(" ", &comptime parseParamsComptime( 2160 | \\ 2161 | \\ 2162 | \\ 2163 | \\ 2164 | )); 2165 | try testUsage(" ...", &comptime parseParamsComptime( 2166 | \\ 2167 | \\ 2168 | \\... 2169 | \\ 2170 | )); 2171 | } 2172 | 2173 | test { 2174 | _ = args; 2175 | _ = parsers; 2176 | _ = streaming; 2177 | _ = ccw; 2178 | } 2179 | 2180 | pub const args = @import("clap/args.zig"); 2181 | pub const parsers = @import("clap/parsers.zig"); 2182 | pub const streaming = @import("clap/streaming.zig"); 2183 | pub const ccw = @import("clap/codepoint_counting_writer.zig"); 2184 | 2185 | const std = @import("std"); 2186 | -------------------------------------------------------------------------------- /src/clap/args.zig: -------------------------------------------------------------------------------- 1 | /// An example of what methods should be implemented on an arg iterator. 2 | pub const ExampleArgIterator = struct { 3 | pub fn next(iter: *ExampleArgIterator) ?[]const u8 { 4 | _ = iter; 5 | return "2"; 6 | } 7 | }; 8 | 9 | /// An argument iterator which iterates over a slice of arguments. 10 | /// This implementation does not allocate. 11 | pub const SliceIterator = struct { 12 | args: []const []const u8, 13 | index: usize = 0, 14 | 15 | pub fn next(iter: *SliceIterator) ?[]const u8 { 16 | if (iter.args.len <= iter.index) 17 | return null; 18 | 19 | defer iter.index += 1; 20 | return iter.args[iter.index]; 21 | } 22 | }; 23 | 24 | test "SliceIterator" { 25 | const args = [_][]const u8{ "A", "BB", "CCC" }; 26 | var iter = SliceIterator{ .args = &args }; 27 | 28 | for (args) |a| 29 | try std.testing.expectEqualStrings(a, iter.next().?); 30 | 31 | try std.testing.expectEqual(@as(?[]const u8, null), iter.next()); 32 | } 33 | 34 | const std = @import("std"); 35 | -------------------------------------------------------------------------------- /src/clap/codepoint_counting_writer.zig: -------------------------------------------------------------------------------- 1 | /// A Writer that counts how many codepoints has been written to it. 2 | /// Expects valid UTF-8 input, and does not validate the input. 3 | pub const CodepointCountingWriter = struct { 4 | codepoints_written: u64 = 0, 5 | child_stream: *std.Io.Writer, 6 | interface: std.Io.Writer = .{ 7 | .buffer = &.{}, 8 | .vtable = &.{ .drain = drain }, 9 | }, 10 | 11 | const Self = @This(); 12 | 13 | pub fn init(child_stream: *std.Io.Writer) Self { 14 | return .{ 15 | .child_stream = child_stream, 16 | }; 17 | } 18 | 19 | fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 20 | const self: *Self = @alignCast(@fieldParentPtr("interface", w)); 21 | var n_bytes_written: usize = 0; 22 | var i: usize = 0; 23 | 24 | while (i < data.len + splat - 1) : (i += 1) { 25 | const chunk = data[@min(i, data.len)]; 26 | const bytes_and_codepoints = utf8CountCodepointsAllowTruncate(chunk) catch return std.Io.Writer.Error.WriteFailed; 27 | // Might not be the full input, so the leftover bytes are written on the next call. 28 | const bytes_to_write = chunk[0..bytes_and_codepoints.bytes]; 29 | const amt = try self.child_stream.write(bytes_to_write); 30 | n_bytes_written += amt; 31 | const bytes_written = bytes_to_write[0..amt]; 32 | self.codepoints_written += (utf8CountCodepointsAllowTruncate(bytes_written) catch return std.Io.Writer.Error.WriteFailed).codepoints; 33 | } 34 | return n_bytes_written; 35 | } 36 | }; 37 | 38 | // Like `std.unicode.utf8CountCodepoints`, but on truncated input, it returns 39 | // the number of codepoints up to that point. 40 | // Does not validate UTF-8 beyond checking the start byte. 41 | fn utf8CountCodepointsAllowTruncate(s: []const u8) !struct { bytes: usize, codepoints: usize } { 42 | const native_endian = @import("builtin").cpu.arch.endian(); 43 | var len: usize = 0; 44 | 45 | const N = @sizeOf(usize); 46 | const MASK = 0x80 * (std.math.maxInt(usize) / 0xff); 47 | 48 | var i: usize = 0; 49 | while (i < s.len) { 50 | // Fast path for ASCII sequences 51 | while (i + N <= s.len) : (i += N) { 52 | const v = std.mem.readInt(usize, s[i..][0..N], native_endian); 53 | if (v & MASK != 0) break; 54 | len += N; 55 | } 56 | 57 | if (i < s.len) { 58 | const n = try std.unicode.utf8ByteSequenceLength(s[i]); 59 | // Truncated input; return the current counts. 60 | if (i + n > s.len) return .{ .bytes = i, .codepoints = len }; 61 | 62 | i += n; 63 | len += 1; 64 | } 65 | } 66 | 67 | return .{ .bytes = i, .codepoints = len }; 68 | } 69 | 70 | const testing = std.testing; 71 | 72 | test CodepointCountingWriter { 73 | var discarding = std.Io.Writer.Discarding.init(&.{}); 74 | var counting_stream = CodepointCountingWriter.init(&discarding.writer); 75 | 76 | const utf8_text = "blåhaj" ** 100; 77 | counting_stream.interface.writeAll(utf8_text) catch unreachable; 78 | const expected_count = try std.unicode.utf8CountCodepoints(utf8_text); 79 | try testing.expectEqual(expected_count, counting_stream.codepoints_written); 80 | } 81 | 82 | test "handles partial UTF-8 writes" { 83 | var buf: [100]u8 = undefined; 84 | var fbs = std.Io.Writer.fixed(&buf); 85 | var counting_stream = CodepointCountingWriter.init(&fbs); 86 | 87 | const utf8_text = "ååå"; 88 | // `å` is represented as `\xC5\xA5`, write 1.5 `å`s. 89 | var wc = try counting_stream.interface.write(utf8_text[0..3]); 90 | // One should have been written fully. 91 | try testing.expectEqual("å".len, wc); 92 | try testing.expectEqual(1, counting_stream.codepoints_written); 93 | 94 | // Write the rest, continuing from the reported number of bytes written. 95 | wc = try counting_stream.interface.write(utf8_text[wc..]); 96 | try testing.expectEqual(4, wc); 97 | try testing.expectEqual(3, counting_stream.codepoints_written); 98 | 99 | const expected_count = try std.unicode.utf8CountCodepoints(utf8_text); 100 | try testing.expectEqual(expected_count, counting_stream.codepoints_written); 101 | 102 | try testing.expectEqualSlices(u8, utf8_text, fbs.buffered()); 103 | } 104 | 105 | const std = @import("std"); 106 | -------------------------------------------------------------------------------- /src/clap/parsers.zig: -------------------------------------------------------------------------------- 1 | pub const default = .{ 2 | .string = string, 3 | .str = string, 4 | .u8 = int(u8, 0), 5 | .u16 = int(u16, 0), 6 | .u32 = int(u32, 0), 7 | .u64 = int(u64, 0), 8 | .usize = int(usize, 0), 9 | .i8 = int(i8, 0), 10 | .i16 = int(i16, 0), 11 | .i32 = int(i32, 0), 12 | .i64 = int(i64, 0), 13 | .isize = int(isize, 0), 14 | .f32 = float(f32), 15 | .f64 = float(f64), 16 | }; 17 | 18 | /// A parser that does nothing. 19 | pub fn string(in: []const u8) error{}![]const u8 { 20 | return in; 21 | } 22 | 23 | test "string" { 24 | try std.testing.expectEqualStrings("aa", try string("aa")); 25 | } 26 | 27 | /// A parser that uses `std.fmt.parseInt` or `std.fmt.parseUnsigned` to parse the string into an integer value. 28 | /// See `std.fmt.parseInt` and `std.fmt.parseUnsigned` documentation for more information. 29 | pub fn int(comptime T: type, comptime base: u8) fn ([]const u8) std.fmt.ParseIntError!T { 30 | return struct { 31 | fn parse(in: []const u8) std.fmt.ParseIntError!T { 32 | return switch (@typeInfo(T).int.signedness) { 33 | .signed => std.fmt.parseInt(T, in, base), 34 | .unsigned => std.fmt.parseUnsigned(T, in, base), 35 | }; 36 | } 37 | }.parse; 38 | } 39 | 40 | test "int" { 41 | try std.testing.expectEqual(@as(i8, 0), try int(i8, 10)("0")); 42 | try std.testing.expectEqual(@as(i8, 1), try int(i8, 10)("1")); 43 | try std.testing.expectEqual(@as(i8, 10), try int(i8, 10)("10")); 44 | try std.testing.expectEqual(@as(i8, 0b10), try int(i8, 2)("10")); 45 | try std.testing.expectEqual(@as(i8, 0x10), try int(i8, 0)("0x10")); 46 | try std.testing.expectEqual(@as(i8, 0b10), try int(i8, 0)("0b10")); 47 | try std.testing.expectEqual(@as(i16, 0), try int(i16, 10)("0")); 48 | try std.testing.expectEqual(@as(i16, 1), try int(i16, 10)("1")); 49 | try std.testing.expectEqual(@as(i16, 10), try int(i16, 10)("10")); 50 | try std.testing.expectEqual(@as(i16, 0b10), try int(i16, 2)("10")); 51 | try std.testing.expectEqual(@as(i16, 0x10), try int(i16, 0)("0x10")); 52 | try std.testing.expectEqual(@as(i16, 0b10), try int(i16, 0)("0b10")); 53 | 54 | try std.testing.expectEqual(@as(i8, 0), try int(i8, 10)("-0")); 55 | try std.testing.expectEqual(@as(i8, -1), try int(i8, 10)("-1")); 56 | try std.testing.expectEqual(@as(i8, -10), try int(i8, 10)("-10")); 57 | try std.testing.expectEqual(@as(i8, -0b10), try int(i8, 2)("-10")); 58 | try std.testing.expectEqual(@as(i8, -0x10), try int(i8, 0)("-0x10")); 59 | try std.testing.expectEqual(@as(i8, -0b10), try int(i8, 0)("-0b10")); 60 | try std.testing.expectEqual(@as(i16, 0), try int(i16, 10)("-0")); 61 | try std.testing.expectEqual(@as(i16, -1), try int(i16, 10)("-1")); 62 | try std.testing.expectEqual(@as(i16, -10), try int(i16, 10)("-10")); 63 | try std.testing.expectEqual(@as(i16, -0b10), try int(i16, 2)("-10")); 64 | try std.testing.expectEqual(@as(i16, -0x10), try int(i16, 0)("-0x10")); 65 | try std.testing.expectEqual(@as(i16, -0b10), try int(i16, 0)("-0b10")); 66 | 67 | try std.testing.expectEqual(@as(u8, 0), try int(u8, 10)("0")); 68 | try std.testing.expectEqual(@as(u8, 1), try int(u8, 10)("1")); 69 | try std.testing.expectEqual(@as(u8, 10), try int(u8, 10)("10")); 70 | try std.testing.expectEqual(@as(u8, 0b10), try int(u8, 2)("10")); 71 | try std.testing.expectEqual(@as(u8, 0x10), try int(u8, 0)("0x10")); 72 | try std.testing.expectEqual(@as(u8, 0b10), try int(u8, 0)("0b10")); 73 | try std.testing.expectEqual(@as(u16, 0), try int(u16, 10)("0")); 74 | try std.testing.expectEqual(@as(u16, 1), try int(u16, 10)("1")); 75 | try std.testing.expectEqual(@as(u16, 10), try int(u16, 10)("10")); 76 | try std.testing.expectEqual(@as(u16, 0b10), try int(u16, 2)("10")); 77 | try std.testing.expectEqual(@as(u16, 0x10), try int(u16, 0)("0x10")); 78 | try std.testing.expectEqual(@as(u16, 0b10), try int(u16, 0)("0b10")); 79 | 80 | try std.testing.expectEqual(std.fmt.ParseIntError.InvalidCharacter, int(u8, 10)("-10")); 81 | } 82 | 83 | /// A parser that uses `std.fmt.parseFloat` to parse the string into an float value. 84 | /// See `std.fmt.parseFloat` documentation for more information. 85 | pub fn float(comptime T: type) fn ([]const u8) std.fmt.ParseFloatError!T { 86 | return struct { 87 | fn parse(in: []const u8) std.fmt.ParseFloatError!T { 88 | return std.fmt.parseFloat(T, in); 89 | } 90 | }.parse; 91 | } 92 | 93 | test "float" { 94 | try std.testing.expectEqual(@as(f32, 0), try float(f32)("0")); 95 | } 96 | 97 | pub const EnumError = error{ 98 | NameNotPartOfEnum, 99 | }; 100 | 101 | /// A parser that uses `std.meta.stringToEnum` to parse the string into an enum value. On `null`, 102 | /// this function returns the error `NameNotPartOfEnum`. 103 | /// See `std.meta.stringToEnum` documentation for more information. 104 | pub fn enumeration(comptime T: type) fn ([]const u8) EnumError!T { 105 | return struct { 106 | fn parse(in: []const u8) EnumError!T { 107 | return std.meta.stringToEnum(T, in) orelse error.NameNotPartOfEnum; 108 | } 109 | }.parse; 110 | } 111 | 112 | test "enumeration" { 113 | const E = enum { a, b, c }; 114 | try std.testing.expectEqual(E.a, try enumeration(E)("a")); 115 | try std.testing.expectEqual(E.b, try enumeration(E)("b")); 116 | try std.testing.expectEqual(E.c, try enumeration(E)("c")); 117 | try std.testing.expectError(EnumError.NameNotPartOfEnum, enumeration(E)("d")); 118 | } 119 | 120 | fn ReturnType(comptime P: type) type { 121 | return @typeInfo(P).@"fn".return_type.?; 122 | } 123 | 124 | pub fn Result(comptime P: type) type { 125 | return @typeInfo(ReturnType(P)).error_union.payload; 126 | } 127 | 128 | const std = @import("std"); 129 | -------------------------------------------------------------------------------- /src/clap/streaming.zig: -------------------------------------------------------------------------------- 1 | /// The result returned from Clap.next 2 | pub fn Arg(comptime Id: type) type { 3 | return struct { 4 | const Self = @This(); 5 | 6 | param: *const clap.Param(Id), 7 | value: ?[]const u8 = null, 8 | }; 9 | } 10 | 11 | pub const Error = error{ 12 | MissingValue, 13 | InvalidArgument, 14 | DoesntTakeValue, 15 | }; 16 | 17 | /// A command line argument parser which, given an ArgIterator, will parse arguments according 18 | /// to the params. Clap parses in an iterating manner, so you have to use a loop together with 19 | /// Clap.next to parse all the arguments of your program. 20 | /// 21 | /// This parser is the building block for all the more complicated parsers. 22 | pub fn Clap(comptime Id: type, comptime ArgIterator: type) type { 23 | return struct { 24 | const State = union(enum) { 25 | normal, 26 | chaining: Chaining, 27 | rest_are_positional, 28 | 29 | const Chaining = struct { 30 | arg: []const u8, 31 | index: usize, 32 | }; 33 | }; 34 | 35 | state: State = .normal, 36 | 37 | params: []const clap.Param(Id), 38 | iter: *ArgIterator, 39 | 40 | positional: ?*const clap.Param(Id) = null, 41 | diagnostic: ?*clap.Diagnostic = null, 42 | assignment_separators: []const u8 = clap.default_assignment_separators, 43 | 44 | /// Get the next Arg that matches a Param. 45 | pub fn next(parser: *@This()) !?Arg(Id) { 46 | switch (parser.state) { 47 | .normal => return try parser.normal(), 48 | .chaining => |state| return try parser.chaining(state), 49 | .rest_are_positional => { 50 | const param = parser.positionalParam() orelse unreachable; 51 | const value = parser.iter.next() orelse return null; 52 | return Arg(Id){ .param = param, .value = value }; 53 | }, 54 | } 55 | } 56 | 57 | fn normal(parser: *@This()) !?Arg(Id) { 58 | const arg_info = (try parser.parseNextArg()) orelse return null; 59 | const arg = arg_info.arg; 60 | switch (arg_info.kind) { 61 | .long => { 62 | const eql_index = std.mem.indexOfAny(u8, arg, parser.assignment_separators); 63 | const name = if (eql_index) |i| arg[0..i] else arg; 64 | const maybe_value = if (eql_index) |i| arg[i + 1 ..] else null; 65 | 66 | for (parser.params) |*param| { 67 | const match = param.names.long orelse continue; 68 | 69 | if (!std.mem.eql(u8, name, match)) 70 | continue; 71 | if (param.takes_value == .none) { 72 | if (maybe_value != null) 73 | return parser.err(arg, .{ .long = name }, Error.DoesntTakeValue); 74 | 75 | return Arg(Id){ .param = param }; 76 | } 77 | 78 | const value = blk: { 79 | if (maybe_value) |v| 80 | break :blk v; 81 | 82 | break :blk parser.iter.next() orelse 83 | return parser.err(arg, .{ .long = name }, Error.MissingValue); 84 | }; 85 | 86 | return Arg(Id){ .param = param, .value = value }; 87 | } 88 | 89 | return parser.err(arg, .{ .long = name }, Error.InvalidArgument); 90 | }, 91 | .short => return try parser.chaining(.{ 92 | .arg = arg, 93 | .index = 0, 94 | }), 95 | .positional => if (parser.positionalParam()) |param| { 96 | // If we find a positional with the value `--` then we 97 | // interpret the rest of the arguments as positional 98 | // arguments. 99 | if (std.mem.eql(u8, arg, "--")) { 100 | parser.state = .rest_are_positional; 101 | const value = parser.iter.next() orelse return null; 102 | return Arg(Id){ .param = param, .value = value }; 103 | } 104 | 105 | return Arg(Id){ .param = param, .value = arg }; 106 | } else { 107 | return parser.err(arg, .{}, Error.InvalidArgument); 108 | }, 109 | } 110 | } 111 | 112 | fn chaining(parser: *@This(), state: State.Chaining) !?Arg(Id) { 113 | const arg = state.arg; 114 | const index = state.index; 115 | const next_index = index + 1; 116 | 117 | for (parser.params) |*param| { 118 | const short = param.names.short orelse continue; 119 | if (short != arg[index]) 120 | continue; 121 | 122 | // Before we return, we have to set the new state of the clap 123 | defer { 124 | if (arg.len <= next_index or param.takes_value != .none) { 125 | parser.state = .normal; 126 | } else { 127 | parser.state = .{ 128 | .chaining = .{ 129 | .arg = arg, 130 | .index = next_index, 131 | }, 132 | }; 133 | } 134 | } 135 | 136 | const next_is_separator = if (next_index < arg.len) 137 | std.mem.indexOfScalar(u8, parser.assignment_separators, arg[next_index]) != null 138 | else 139 | false; 140 | 141 | if (param.takes_value == .none) { 142 | if (next_is_separator) 143 | return parser.err(arg, .{ .short = short }, Error.DoesntTakeValue); 144 | return Arg(Id){ .param = param }; 145 | } 146 | 147 | if (arg.len <= next_index) { 148 | const value = parser.iter.next() orelse 149 | return parser.err(arg, .{ .short = short }, Error.MissingValue); 150 | 151 | return Arg(Id){ .param = param, .value = value }; 152 | } 153 | 154 | if (next_is_separator) 155 | return Arg(Id){ .param = param, .value = arg[next_index + 1 ..] }; 156 | 157 | return Arg(Id){ .param = param, .value = arg[next_index..] }; 158 | } 159 | 160 | return parser.err(arg, .{ .short = arg[index] }, Error.InvalidArgument); 161 | } 162 | 163 | fn positionalParam(parser: *@This()) ?*const clap.Param(Id) { 164 | if (parser.positional) |p| 165 | return p; 166 | 167 | for (parser.params) |*param| { 168 | if (param.names.long) |_| 169 | continue; 170 | if (param.names.short) |_| 171 | continue; 172 | 173 | parser.positional = param; 174 | return param; 175 | } 176 | 177 | return null; 178 | } 179 | 180 | const ArgInfo = struct { 181 | arg: []const u8, 182 | kind: enum { 183 | long, 184 | short, 185 | positional, 186 | }, 187 | }; 188 | 189 | fn parseNextArg(parser: *@This()) !?ArgInfo { 190 | const full_arg = parser.iter.next() orelse return null; 191 | if (std.mem.eql(u8, full_arg, "--") or std.mem.eql(u8, full_arg, "-")) 192 | return ArgInfo{ .arg = full_arg, .kind = .positional }; 193 | if (std.mem.startsWith(u8, full_arg, "--")) 194 | return ArgInfo{ .arg = full_arg[2..], .kind = .long }; 195 | if (std.mem.startsWith(u8, full_arg, "-")) 196 | return ArgInfo{ .arg = full_arg[1..], .kind = .short }; 197 | 198 | return ArgInfo{ .arg = full_arg, .kind = .positional }; 199 | } 200 | 201 | fn err(parser: @This(), arg: []const u8, names: clap.Names, _err: anytype) @TypeOf(_err) { 202 | if (parser.diagnostic) |d| 203 | d.* = .{ .arg = arg, .name = names }; 204 | return _err; 205 | } 206 | }; 207 | } 208 | 209 | fn expectArgs( 210 | parser: *Clap(u8, clap.args.SliceIterator), 211 | results: []const Arg(u8), 212 | ) !void { 213 | for (results) |res| { 214 | const arg = (try parser.next()) orelse return error.TestFailed; 215 | try std.testing.expectEqual(res.param, arg.param); 216 | const expected_value = res.value orelse { 217 | try std.testing.expectEqual(@as(@TypeOf(arg.value), null), arg.value); 218 | continue; 219 | }; 220 | const actual_value = arg.value orelse return error.TestFailed; 221 | try std.testing.expectEqualSlices(u8, expected_value, actual_value); 222 | } 223 | 224 | if (try parser.next()) |_| 225 | return error.TestFailed; 226 | } 227 | 228 | fn expectError( 229 | parser: *Clap(u8, clap.args.SliceIterator), 230 | expected: []const u8, 231 | ) !void { 232 | var diag: clap.Diagnostic = .{}; 233 | parser.diagnostic = &diag; 234 | 235 | while (parser.next() catch |err| { 236 | var buf: [1024]u8 = undefined; 237 | var fbs = std.Io.Writer.fixed(&buf); 238 | try diag.report(&fbs, err); 239 | try std.testing.expectEqualStrings(expected, fbs.buffered()); 240 | return; 241 | }) |_| {} 242 | 243 | try std.testing.expect(false); 244 | } 245 | 246 | test "short params" { 247 | const params = [_]clap.Param(u8){ 248 | .{ .id = 0, .names = .{ .short = 'a' } }, 249 | .{ .id = 1, .names = .{ .short = 'b' } }, 250 | .{ 251 | .id = 2, 252 | .names = .{ .short = 'c' }, 253 | .takes_value = .one, 254 | }, 255 | .{ 256 | .id = 3, 257 | .names = .{ .short = 'd' }, 258 | .takes_value = .many, 259 | }, 260 | }; 261 | 262 | const a = ¶ms[0]; 263 | const b = ¶ms[1]; 264 | const c = ¶ms[2]; 265 | const d = ¶ms[3]; 266 | 267 | var iter = clap.args.SliceIterator{ .args = &.{ 268 | "-a", "-b", "-ab", "-ba", 269 | "-c", "0", "-c=0", "-ac", 270 | "0", "-ac=0", "-d=0", 271 | } }; 272 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 273 | 274 | try expectArgs(&parser, &.{ 275 | .{ .param = a }, 276 | .{ .param = b }, 277 | .{ .param = a }, 278 | .{ .param = b }, 279 | .{ .param = b }, 280 | .{ .param = a }, 281 | .{ .param = c, .value = "0" }, 282 | .{ .param = c, .value = "0" }, 283 | .{ .param = a }, 284 | .{ .param = c, .value = "0" }, 285 | .{ .param = a }, 286 | .{ .param = c, .value = "0" }, 287 | .{ .param = d, .value = "0" }, 288 | }); 289 | } 290 | 291 | test "long params" { 292 | const params = [_]clap.Param(u8){ 293 | .{ .id = 0, .names = .{ .long = "aa" } }, 294 | .{ .id = 1, .names = .{ .long = "bb" } }, 295 | .{ 296 | .id = 2, 297 | .names = .{ .long = "cc" }, 298 | .takes_value = .one, 299 | }, 300 | .{ 301 | .id = 3, 302 | .names = .{ .long = "dd" }, 303 | .takes_value = .many, 304 | }, 305 | }; 306 | 307 | const aa = ¶ms[0]; 308 | const bb = ¶ms[1]; 309 | const cc = ¶ms[2]; 310 | const dd = ¶ms[3]; 311 | 312 | var iter = clap.args.SliceIterator{ .args = &.{ 313 | "--aa", "--bb", 314 | "--cc", "0", 315 | "--cc=0", "--dd=0", 316 | } }; 317 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 318 | 319 | try expectArgs(&parser, &.{ 320 | .{ .param = aa }, 321 | .{ .param = bb }, 322 | .{ .param = cc, .value = "0" }, 323 | .{ .param = cc, .value = "0" }, 324 | .{ .param = dd, .value = "0" }, 325 | }); 326 | } 327 | 328 | test "positional params" { 329 | const params = [_]clap.Param(u8){.{ 330 | .id = 0, 331 | .takes_value = .one, 332 | }}; 333 | 334 | var iter = clap.args.SliceIterator{ .args = &.{ 335 | "aa", 336 | "bb", 337 | } }; 338 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 339 | 340 | try expectArgs(&parser, &.{ 341 | .{ .param = ¶ms[0], .value = "aa" }, 342 | .{ .param = ¶ms[0], .value = "bb" }, 343 | }); 344 | } 345 | 346 | test "all params" { 347 | const params = [_]clap.Param(u8){ 348 | .{ 349 | .id = 0, 350 | .names = .{ .short = 'a', .long = "aa" }, 351 | }, 352 | .{ 353 | .id = 1, 354 | .names = .{ .short = 'b', .long = "bb" }, 355 | }, 356 | .{ 357 | .id = 2, 358 | .names = .{ .short = 'c', .long = "cc" }, 359 | .takes_value = .one, 360 | }, 361 | .{ .id = 3, .takes_value = .one }, 362 | }; 363 | 364 | const aa = ¶ms[0]; 365 | const bb = ¶ms[1]; 366 | const cc = ¶ms[2]; 367 | const positional = ¶ms[3]; 368 | 369 | var iter = clap.args.SliceIterator{ .args = &.{ 370 | "-a", "-b", "-ab", "-ba", 371 | "-c", "0", "-c=0", "-ac", 372 | "0", "-ac=0", "--aa", "--bb", 373 | "--cc", "0", "--cc=0", "something", 374 | "-", "--", "--cc=0", "-a", 375 | } }; 376 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 377 | 378 | try expectArgs(&parser, &.{ 379 | .{ .param = aa }, 380 | .{ .param = bb }, 381 | .{ .param = aa }, 382 | .{ .param = bb }, 383 | .{ .param = bb }, 384 | .{ .param = aa }, 385 | .{ .param = cc, .value = "0" }, 386 | .{ .param = cc, .value = "0" }, 387 | .{ .param = aa }, 388 | .{ .param = cc, .value = "0" }, 389 | .{ .param = aa }, 390 | .{ .param = cc, .value = "0" }, 391 | .{ .param = aa }, 392 | .{ .param = bb }, 393 | .{ .param = cc, .value = "0" }, 394 | .{ .param = cc, .value = "0" }, 395 | .{ .param = positional, .value = "something" }, 396 | .{ .param = positional, .value = "-" }, 397 | .{ .param = positional, .value = "--cc=0" }, 398 | .{ .param = positional, .value = "-a" }, 399 | }); 400 | } 401 | 402 | test "different assignment separators" { 403 | const params = [_]clap.Param(u8){ 404 | .{ 405 | .id = 0, 406 | .names = .{ .short = 'a', .long = "aa" }, 407 | .takes_value = .one, 408 | }, 409 | }; 410 | 411 | const aa = ¶ms[0]; 412 | 413 | var iter = clap.args.SliceIterator{ .args = &.{ 414 | "-a=0", "--aa=0", 415 | "-a:0", "--aa:0", 416 | } }; 417 | var parser = Clap(u8, clap.args.SliceIterator){ 418 | .params = ¶ms, 419 | .iter = &iter, 420 | .assignment_separators = "=:", 421 | }; 422 | 423 | try expectArgs(&parser, &.{ 424 | .{ .param = aa, .value = "0" }, 425 | .{ .param = aa, .value = "0" }, 426 | .{ .param = aa, .value = "0" }, 427 | .{ .param = aa, .value = "0" }, 428 | }); 429 | } 430 | 431 | test "errors" { 432 | const params = [_]clap.Param(u8){ 433 | .{ 434 | .id = 0, 435 | .names = .{ .short = 'a', .long = "aa" }, 436 | }, 437 | .{ 438 | .id = 1, 439 | .names = .{ .short = 'c', .long = "cc" }, 440 | .takes_value = .one, 441 | }, 442 | }; 443 | 444 | var iter = clap.args.SliceIterator{ .args = &.{"q"} }; 445 | var parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 446 | try expectError(&parser, "Invalid argument 'q'\n"); 447 | 448 | iter = clap.args.SliceIterator{ .args = &.{"-q"} }; 449 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 450 | try expectError(&parser, "Invalid argument '-q'\n"); 451 | 452 | iter = clap.args.SliceIterator{ .args = &.{"--q"} }; 453 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 454 | try expectError(&parser, "Invalid argument '--q'\n"); 455 | 456 | iter = clap.args.SliceIterator{ .args = &.{"--q=1"} }; 457 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 458 | try expectError(&parser, "Invalid argument '--q'\n"); 459 | 460 | iter = clap.args.SliceIterator{ .args = &.{"-a=1"} }; 461 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 462 | try expectError(&parser, "The argument '-a' does not take a value\n"); 463 | 464 | iter = clap.args.SliceIterator{ .args = &.{"--aa=1"} }; 465 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 466 | try expectError(&parser, "The argument '--aa' does not take a value\n"); 467 | 468 | iter = clap.args.SliceIterator{ .args = &.{"-c"} }; 469 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 470 | try expectError(&parser, "The argument '-c' requires a value but none was supplied\n"); 471 | 472 | iter = clap.args.SliceIterator{ .args = &.{"--cc"} }; 473 | parser = Clap(u8, clap.args.SliceIterator){ .params = ¶ms, .iter = &iter }; 474 | try expectError(&parser, "The argument '--cc' requires a value but none was supplied\n"); 475 | } 476 | 477 | const clap = @import("../clap.zig"); 478 | const std = @import("std"); 479 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const base64 = std.base64; 3 | const crypto = std.crypto; 4 | const fs = std.fs; 5 | const fmt = std.fmt; 6 | const heap = std.heap; 7 | const io = std.io; 8 | const math = std.math; 9 | const mem = std.mem; 10 | const os = std.os; 11 | const process = std.process; 12 | const Blake2b256 = crypto.hash.blake2.Blake2b256; 13 | const Blake2b512 = crypto.hash.blake2.Blake2b512; 14 | const Ed25519 = crypto.sign.Ed25519; 15 | const Endian = std.builtin.Endian; 16 | 17 | pub const Signature = struct { 18 | arena: heap.ArenaAllocator, 19 | untrusted_comment: []u8, 20 | signature_algorithm: [2]u8, 21 | key_id: [8]u8, 22 | signature: [64]u8, 23 | trusted_comment: []u8, 24 | global_signature: [64]u8, 25 | 26 | pub fn deinit(self: *Signature) void { 27 | self.arena.deinit(); 28 | } 29 | 30 | pub const Algorithm = enum { Prehash, Legacy }; 31 | 32 | pub fn algorithm(sig: Signature) !Algorithm { 33 | const signature_algorithm = sig.signature_algorithm; 34 | const prehashed = if (signature_algorithm[0] == 0x45 and signature_algorithm[1] == 0x64) false else if (signature_algorithm[0] == 0x45 and signature_algorithm[1] == 0x44) true else return error.UnsupportedAlgorithm; 35 | return if (prehashed) .Prehash else .Legacy; 36 | } 37 | 38 | pub fn decode(child_allocator: mem.Allocator, lines_str: []const u8) !Signature { 39 | var arena = heap.ArenaAllocator.init(child_allocator); 40 | errdefer arena.deinit(); 41 | const allocator = arena.allocator(); 42 | var it = mem.tokenizeScalar(u8, lines_str, '\n'); 43 | const untrusted_comment = try allocator.dupe(u8, it.next() orelse return error.InvalidEncoding); 44 | var bin1: [74]u8 = undefined; 45 | try base64.standard.Decoder.decode(&bin1, it.next() orelse return error.InvalidEncoding); 46 | var trusted_comment = try allocator.dupe(u8, it.next() orelse return error.InvalidEncoding); 47 | if (!mem.startsWith(u8, trusted_comment, "trusted comment: ")) { 48 | return error.InvalidEncoding; 49 | } 50 | trusted_comment = trusted_comment["Trusted comment: ".len..]; 51 | var bin2: [64]u8 = undefined; 52 | try base64.standard.Decoder.decode(&bin2, it.next() orelse return error.InvalidEncoding); 53 | const sig = Signature{ 54 | .arena = arena, 55 | .untrusted_comment = untrusted_comment, 56 | .signature_algorithm = bin1[0..2].*, 57 | .key_id = bin1[2..10].*, 58 | .signature = bin1[10..74].*, 59 | .trusted_comment = trusted_comment, 60 | .global_signature = bin2, 61 | }; 62 | return sig; 63 | } 64 | 65 | pub fn fromFile(allocator: mem.Allocator, path: []const u8) !Signature { 66 | const fd = try fs.cwd().openFile(path, .{ .mode = .read_only }); 67 | defer fd.close(); 68 | var file_reader = fd.reader(&.{}); 69 | const sig_str = try file_reader.interface.allocRemaining(allocator, .limited(4096)); 70 | defer allocator.free(sig_str); 71 | return Signature.decode(allocator, sig_str); 72 | } 73 | 74 | pub fn toFile(self: *const Signature, path: []const u8, untrusted_comment: []const u8) !void { 75 | const fd = try fs.cwd().createFile(path, .{ .exclusive = false }); 76 | defer fd.close(); 77 | 78 | var buf: [4096]u8 = undefined; 79 | var file_writer = fd.writer(&buf); 80 | const writer = &file_writer.interface; 81 | 82 | // Write untrusted comment 83 | const comment_prefix = "untrusted comment: "; 84 | try writer.writeAll(comment_prefix); 85 | try writer.writeAll(untrusted_comment); 86 | try writer.writeAll("\n"); 87 | 88 | // Write signature (base64 encoded) 89 | var sig_bin: [74]u8 = undefined; 90 | @memcpy(sig_bin[0..2], &self.signature_algorithm); 91 | @memcpy(sig_bin[2..10], &self.key_id); 92 | @memcpy(sig_bin[10..74], &self.signature); 93 | 94 | const Base64Encoder = base64.standard.Encoder; 95 | var sig_b64: [Base64Encoder.calcSize(74)]u8 = undefined; 96 | _ = Base64Encoder.encode(&sig_b64, &sig_bin); 97 | try writer.writeAll(&sig_b64); 98 | try writer.writeAll("\n"); 99 | 100 | // Write trusted comment 101 | const trusted_prefix = "trusted comment: "; 102 | try writer.writeAll(trusted_prefix); 103 | try writer.writeAll(self.trusted_comment); 104 | try writer.writeAll("\n"); 105 | 106 | // Write global signature (base64 encoded) 107 | var global_b64: [Base64Encoder.calcSize(64)]u8 = undefined; 108 | _ = Base64Encoder.encode(&global_b64, &self.global_signature); 109 | try writer.writeAll(&global_b64); 110 | try writer.writeAll("\n"); 111 | 112 | try writer.flush(); 113 | } 114 | }; 115 | 116 | pub const PublicKey = struct { 117 | untrusted_comment: ?[]u8 = null, 118 | signature_algorithm: [2]u8 = "Ed".*, 119 | key_id: [8]u8, 120 | key: [key_length]u8, 121 | 122 | const key_length = 32; 123 | const key_type = "ssh-ed25519"; 124 | const key_id_prefix = "minisign key "; 125 | 126 | pub fn decodeFromBase64(str: []const u8) !PublicKey { 127 | if (str.len != 56) { 128 | return error.InvalidEncoding; 129 | } 130 | var bin: [42]u8 = undefined; 131 | try base64.standard.Decoder.decode(&bin, str); 132 | const signature_algorithm = bin[0..2]; 133 | if (bin[0] != 0x45 or (bin[1] != 0x64 and bin[1] != 0x44)) { 134 | return error.UnsupportedAlgorithm; 135 | } 136 | const pk = PublicKey{ 137 | .signature_algorithm = signature_algorithm.*, 138 | .key_id = bin[2..10].*, 139 | .key = bin[10..42].*, 140 | }; 141 | return pk; 142 | } 143 | 144 | pub fn decodeFromSsh(pks: []PublicKey, lines: []const u8) ![]PublicKey { 145 | var lines_it = mem.tokenizeScalar(u8, lines, '\n'); 146 | var i: usize = 0; 147 | while (lines_it.next()) |line| { 148 | var pk = PublicKey{ .key_id = @splat(0), .key = undefined }; 149 | 150 | var it = mem.tokenizeScalar(u8, line, ' '); 151 | const header = it.next() orelse return error.InvalidEncoding; 152 | if (!mem.eql(u8, key_type, header)) { 153 | return error.InvalidEncoding; 154 | } 155 | const encoded_ssh_key = it.next() orelse return error.InvalidEncoding; 156 | const pk_len = pk.key.len; 157 | var ssh_key: [4 + key_type.len + 4 + pk_len]u8 = undefined; 158 | try base64.standard.Decoder.decode(&ssh_key, encoded_ssh_key); 159 | if (mem.readInt(u32, ssh_key[0..4], Endian.big) != key_type.len or 160 | !mem.eql(u8, ssh_key[4..][0..key_type.len], key_type) or 161 | mem.readInt(u32, ssh_key[4 + key_type.len ..][0..4], Endian.big) != pk.key.len) 162 | { 163 | return error.InvalidEncoding; 164 | } 165 | @memcpy(&pk.key, ssh_key[4 + key_type.len + 4 ..]); 166 | 167 | const rest = mem.trim(u8, it.rest(), " \t\r\n"); 168 | if (mem.startsWith(u8, rest, key_id_prefix) and rest.len > key_id_prefix.len) { 169 | mem.writeInt(u64, &pk.key_id, try fmt.parseInt(u64, rest[key_id_prefix.len..], 16), Endian.little); 170 | } 171 | pks[i] = pk; 172 | i += 1; 173 | if (i == pks.len) break; 174 | } 175 | if (i == 0) { 176 | return error.InvalidEncoding; 177 | } 178 | return pks[0..i]; 179 | } 180 | 181 | pub fn decode(pks: []PublicKey, lines_str: []const u8) ![]PublicKey { 182 | if (decodeFromSsh(pks, lines_str)) |pks_| return pks_ else |_| {} 183 | 184 | var it = mem.tokenizeScalar(u8, lines_str, '\n'); 185 | _ = it.next() orelse return error.InvalidEncoding; 186 | const pk = try decodeFromBase64(it.next() orelse return error.InvalidEncoding); 187 | pks[0] = pk; 188 | return pks[0..1]; 189 | } 190 | 191 | pub fn fromFile(allocator: mem.Allocator, pks: []PublicKey, path: []const u8) ![]PublicKey { 192 | const fd = try fs.cwd().openFile(path, .{ .mode = .read_only }); 193 | defer fd.close(); 194 | var file_reader = fd.reader(&.{}); 195 | const pk_str = try file_reader.interface.allocRemaining(allocator, .limited(4096)); 196 | defer allocator.free(pk_str); 197 | return PublicKey.decode(pks, pk_str); 198 | } 199 | 200 | pub fn verifier(self: *const PublicKey, sig: *const Signature) !Verifier { 201 | const key_id_len = self.key_id.len; 202 | const null_key_id: [key_id_len]u8 = @splat(0); 203 | if (!mem.eql(u8, &null_key_id, &self.key_id) and !mem.eql(u8, &sig.key_id, &self.key_id)) { 204 | return error.KeyIdMismatch; 205 | } 206 | 207 | const ed25519_pk = try Ed25519.PublicKey.fromBytes(self.key); 208 | 209 | return Verifier{ 210 | .pk = self, 211 | .sig = sig, 212 | .format = switch (try sig.algorithm()) { 213 | .Prehash => .{ .Prehash = Blake2b512.init(.{}) }, 214 | .Legacy => .{ .Legacy = try Ed25519.Signature.fromBytes(sig.signature).verifier(ed25519_pk) }, 215 | }, 216 | }; 217 | } 218 | 219 | pub fn verifyFile(self: PublicKey, allocator: std.mem.Allocator, fd: fs.File, sig: Signature, prehash: ?bool) !void { 220 | var v = try self.verifier(&sig); 221 | 222 | if (prehash) |want_prehashed| { 223 | if (want_prehashed and v.format != .Prehash) { 224 | return error.SignatureVerificationFailed; 225 | } 226 | } 227 | 228 | var buf: [heap.page_size_max]u8 = undefined; 229 | while (true) { 230 | const read_nb = try fd.read(&buf); 231 | if (read_nb == 0) { 232 | break; 233 | } 234 | v.update(buf[0..read_nb]); 235 | } 236 | try v.verify(allocator); 237 | } 238 | 239 | pub fn getSshKeyLength() usize { 240 | const bin_len = 4 + key_type.len + 4 + key_length; 241 | const encoded_key_len = base64.standard.Encoder.calcSize(bin_len); 242 | 243 | return key_type.len + 1 + encoded_key_len + 1 + key_id_prefix.len + 16 + 1; 244 | } 245 | 246 | pub fn getSshKey(pk: PublicKey) [getSshKeyLength()]u8 { 247 | var ssh_key: [PublicKey.getSshKeyLength()]u8 = undefined; 248 | pk.encodeToSsh(&ssh_key); 249 | return ssh_key; 250 | } 251 | 252 | pub fn encodeToSsh(pk: PublicKey, buffer: *[getSshKeyLength()]u8) void { 253 | var ssh_key: [4 + key_type.len + 4 + key_length]u8 = undefined; 254 | mem.writeInt(u32, ssh_key[0..4], key_type.len, Endian.big); 255 | @memcpy(ssh_key[4..][0..key_type.len], key_type); 256 | mem.writeInt(u32, ssh_key[4 + key_type.len ..][0..4], pk.key.len, Endian.big); 257 | @memcpy(ssh_key[4 + key_type.len + 4 ..], &pk.key); 258 | 259 | const Base64Encoder = base64.standard.Encoder; 260 | var encoded_ssh_key: [Base64Encoder.calcSize(ssh_key.len)]u8 = undefined; 261 | _ = Base64Encoder.encode(&encoded_ssh_key, &ssh_key); 262 | 263 | _ = fmt.bufPrint(buffer, "{s} {s} {s}{X}\n", .{ key_type, encoded_ssh_key, key_id_prefix, mem.readInt(u64, &pk.key_id, Endian.little) }) catch unreachable; 264 | } 265 | 266 | pub fn toFile(self: PublicKey, path: []const u8) !void { 267 | const fd = try fs.cwd().createFile(path, .{ .exclusive = true }); 268 | defer fd.close(); 269 | 270 | var buf: [256]u8 = undefined; 271 | var file_writer = fd.writer(&buf); 272 | const writer = &file_writer.interface; 273 | 274 | // Write untrusted comment 275 | const comment = "untrusted comment: minisign public key"; 276 | try writer.writeAll(comment); 277 | try writer.writeAll("\n"); 278 | 279 | // Encode and write public key 280 | var bin: [42]u8 = undefined; 281 | @memcpy(bin[0..2], &self.signature_algorithm); 282 | @memcpy(bin[2..10], &self.key_id); 283 | @memcpy(bin[10..42], &self.key); 284 | 285 | const Base64Encoder = base64.standard.Encoder; 286 | var encoded: [Base64Encoder.calcSize(42)]u8 = undefined; 287 | _ = Base64Encoder.encode(&encoded, &bin); 288 | try writer.writeAll(&encoded); 289 | try writer.writeAll("\n"); 290 | 291 | try writer.flush(); 292 | } 293 | }; 294 | 295 | pub const Verifier = struct { 296 | pk: *const PublicKey, 297 | sig: *const Signature, 298 | format: union(enum) { 299 | Prehash: Blake2b512, 300 | Legacy: Ed25519.Verifier, 301 | }, 302 | 303 | pub fn update(self: *Verifier, bytes: []const u8) void { 304 | switch (self.format) { 305 | .Prehash => |*prehash| prehash.update(bytes), 306 | .Legacy => |*legacy| legacy.update(bytes), 307 | } 308 | } 309 | 310 | pub fn verify(self: *Verifier, allocator: std.mem.Allocator) !void { 311 | const ed25519_pk = try Ed25519.PublicKey.fromBytes(self.pk.key); 312 | switch (self.format) { 313 | .Prehash => |*prehash| { 314 | var digest: [64]u8 = undefined; 315 | 316 | prehash.final(&digest); 317 | 318 | try Ed25519.Signature.fromBytes(self.sig.signature).verify(&digest, ed25519_pk); 319 | }, 320 | .Legacy => |*legacy| { 321 | try legacy.verify(); 322 | }, 323 | } 324 | 325 | var global = try allocator.alloc(u8, self.sig.signature.len + self.sig.trusted_comment.len); 326 | defer allocator.free(global); 327 | @memcpy(global[0..self.sig.signature.len], self.sig.signature[0..]); 328 | @memcpy(global[self.sig.signature.len..], self.sig.trusted_comment); 329 | try Ed25519.Signature.fromBytes(self.sig.global_signature).verify(global, ed25519_pk); 330 | } 331 | }; 332 | 333 | pub const SecretKey = struct { 334 | arena: heap.ArenaAllocator, 335 | untrusted_comment: []u8, 336 | signature_algorithm: [2]u8, 337 | kdf_algorithm: [2]u8, 338 | checksum_algorithm: [2]u8, 339 | kdf_salt: [32]u8, 340 | kdf_opslimit: u64, 341 | kdf_memlimit: u64, 342 | key_id: [8]u8, 343 | secret_key: [64]u8, 344 | checksum: [32]u8, 345 | 346 | pub fn deinit(self: *SecretKey) void { 347 | crypto.secureZero(u8, &self.secret_key); 348 | crypto.secureZero(u8, &self.checksum); 349 | self.arena.deinit(); 350 | } 351 | 352 | pub fn decode(child_allocator: mem.Allocator, lines_str: []const u8) !SecretKey { 353 | var arena = heap.ArenaAllocator.init(child_allocator); 354 | errdefer arena.deinit(); 355 | const allocator = arena.allocator(); 356 | 357 | var it = mem.tokenizeScalar(u8, lines_str, '\n'); 358 | const untrusted_comment = try allocator.dupe(u8, it.next() orelse return error.InvalidEncoding); 359 | 360 | const encoded_key = it.next() orelse return error.InvalidEncoding; 361 | 362 | // The secret key structure is 158 bytes total: 363 | // 2 (sig_alg) + 2 (kdf_alg) + 2 (chk_alg) + 32 (salt) + 8 (opslimit) + 8 (memlimit) + 8 (keynum) + 64 (sk) + 32 (chk) = 158 bytes 364 | // The encrypted part is: 8 (keynum) + 64 (sk) + 32 (chk) = 104 bytes 365 | // Total in file: 2 + 2 + 2 + 32 + 8 + 8 + 104 = 158 bytes 366 | var bin: [158]u8 = undefined; 367 | try base64.standard.Decoder.decode(&bin, encoded_key); 368 | 369 | var sk = SecretKey{ 370 | .arena = arena, 371 | .untrusted_comment = untrusted_comment, 372 | .signature_algorithm = bin[0..2].*, 373 | .kdf_algorithm = bin[2..4].*, 374 | .checksum_algorithm = bin[4..6].*, 375 | .kdf_salt = bin[6..38].*, 376 | .kdf_opslimit = mem.readInt(u64, bin[38..46], Endian.little), 377 | .kdf_memlimit = mem.readInt(u64, bin[46..54], Endian.little), 378 | .key_id = bin[54..62].*, 379 | .secret_key = bin[62..126].*, 380 | .checksum = bin[126..158].*, 381 | }; 382 | 383 | if (!mem.eql(u8, &sk.signature_algorithm, "Ed")) { 384 | return error.UnsupportedAlgorithm; 385 | } 386 | if (!mem.eql(u8, &sk.checksum_algorithm, "B2")) { 387 | return error.UnsupportedChecksumAlgorithm; 388 | } 389 | 390 | return sk; 391 | } 392 | 393 | pub fn fromFile(allocator: mem.Allocator, path: []const u8) !SecretKey { 394 | const fd = try fs.cwd().openFile(path, .{ .mode = .read_only }); 395 | defer fd.close(); 396 | var file_reader = fd.reader(&.{}); 397 | const sk_str = try file_reader.interface.allocRemaining(allocator, .limited(4096)); 398 | defer allocator.free(sk_str); 399 | return SecretKey.decode(allocator, sk_str); 400 | } 401 | 402 | fn xorData(self: *SecretKey, stream: []const u8) void { 403 | var data: [104]u8 = undefined; 404 | @memcpy(data[0..8], &self.key_id); 405 | @memcpy(data[8..72], &self.secret_key); 406 | @memcpy(data[72..104], &self.checksum); 407 | 408 | for (&data, stream) |*byte, key| byte.* ^= key; 409 | 410 | @memcpy(&self.key_id, data[0..8]); 411 | @memcpy(&self.secret_key, data[8..72]); 412 | @memcpy(&self.checksum, data[72..104]); 413 | } 414 | 415 | pub fn decrypt(self: *SecretKey, allocator: mem.Allocator, password: []const u8) !void { 416 | if (mem.eql(u8, &self.kdf_algorithm, "\x00\x00")) return; 417 | if (!mem.eql(u8, &self.kdf_algorithm, "Sc")) return error.UnsupportedKdfAlgorithm; 418 | 419 | var stream: [104]u8 = undefined; 420 | defer crypto.secureZero(u8, &stream); 421 | 422 | const params = crypto.pwhash.scrypt.Params.fromLimits(self.kdf_opslimit, @intCast(self.kdf_memlimit)); 423 | try crypto.pwhash.scrypt.kdf(allocator, &stream, password, &self.kdf_salt, params); 424 | 425 | self.xorData(&stream); 426 | 427 | // Verify checksum 428 | var computed_checksum: [32]u8 = undefined; 429 | var hasher = Blake2b256.init(.{}); 430 | hasher.update(&self.signature_algorithm); 431 | hasher.update(&self.key_id); 432 | hasher.update(&self.secret_key); 433 | hasher.final(&computed_checksum); 434 | 435 | if (!crypto.timing_safe.eql([32]u8, computed_checksum, self.checksum)) { 436 | crypto.secureZero(u8, &self.secret_key); 437 | return error.WrongPassword; 438 | } 439 | } 440 | 441 | pub fn signFile( 442 | self: *const SecretKey, 443 | allocator: mem.Allocator, 444 | fd: fs.File, 445 | prehash: bool, 446 | trusted_comment: []const u8, 447 | ) !Signature { 448 | if (!prehash) return error.LegacySigningNotImplemented; 449 | 450 | var message: [64]u8 = undefined; 451 | var hasher = Blake2b512.init(.{}); 452 | var buf: [heap.page_size_max]u8 = undefined; 453 | while (true) { 454 | const read_nb = try fd.read(&buf); 455 | if (read_nb == 0) break; 456 | hasher.update(buf[0..read_nb]); 457 | } 458 | hasher.final(&message); 459 | 460 | const ed25519_sk = Ed25519.SecretKey{ .bytes = self.secret_key }; 461 | const keypair = Ed25519.KeyPair{ 462 | .public_key = try Ed25519.PublicKey.fromBytes(ed25519_sk.publicKeyBytes()), 463 | .secret_key = ed25519_sk, 464 | }; 465 | 466 | const sig_bytes = try keypair.sign(&message, null); 467 | 468 | const global_data = try allocator.alloc(u8, 64 + trusted_comment.len); 469 | defer allocator.free(global_data); 470 | @memcpy(global_data[0..64], &sig_bytes.toBytes()); 471 | @memcpy(global_data[64..], trusted_comment); 472 | 473 | var sig_arena = heap.ArenaAllocator.init(allocator); 474 | errdefer sig_arena.deinit(); 475 | 476 | return Signature{ 477 | .arena = sig_arena, 478 | .untrusted_comment = try sig_arena.allocator().dupe(u8, ""), 479 | .signature_algorithm = "ED".*, 480 | .key_id = self.key_id, 481 | .signature = sig_bytes.toBytes(), 482 | .trusted_comment = try sig_arena.allocator().dupe(u8, trusted_comment), 483 | .global_signature = (try keypair.sign(global_data, null)).toBytes(), 484 | }; 485 | } 486 | 487 | pub fn getPublicKey(self: *const SecretKey) PublicKey { 488 | const pk_bytes = self.secret_key[32..64]; 489 | return PublicKey{ 490 | .signature_algorithm = "Ed".*, 491 | .key_id = self.key_id, 492 | .key = pk_bytes.*, 493 | }; 494 | } 495 | 496 | pub fn generate(allocator: mem.Allocator) !SecretKey { 497 | // Generate Ed25519 keypair 498 | const keypair = Ed25519.KeyPair.generate(); 499 | 500 | // Generate random key ID 501 | var key_id: [8]u8 = undefined; 502 | crypto.random.bytes(&key_id); 503 | 504 | // The Ed25519 secret key already contains seed (32) + public key (32) = 64 bytes 505 | const secret_key = keypair.secret_key.bytes; 506 | 507 | // Compute checksum: Blake2b-256(signature_algorithm || key_id || secret_key) 508 | var checksum: [32]u8 = undefined; 509 | var hasher = Blake2b256.init(.{}); 510 | const sig_alg = "Ed".*; 511 | hasher.update(&sig_alg); 512 | hasher.update(&key_id); 513 | hasher.update(&secret_key); 514 | hasher.final(&checksum); 515 | 516 | var arena = heap.ArenaAllocator.init(allocator); 517 | errdefer arena.deinit(); 518 | 519 | return SecretKey{ 520 | .arena = arena, 521 | .untrusted_comment = try arena.allocator().dupe(u8, "untrusted comment: minisign encrypted secret key"), 522 | .signature_algorithm = sig_alg, 523 | .kdf_algorithm = "\x00\x00".*, // Unencrypted by default 524 | .checksum_algorithm = "B2".*, 525 | .kdf_salt = @splat(0), 526 | .kdf_opslimit = 0, 527 | .kdf_memlimit = 0, 528 | .key_id = key_id, 529 | .secret_key = secret_key, 530 | .checksum = checksum, 531 | }; 532 | } 533 | 534 | pub fn encrypt(self: *SecretKey, allocator: mem.Allocator, password: []const u8) !void { 535 | if (password.len == 0) return error.EmptyPassword; 536 | 537 | crypto.random.bytes(&self.kdf_salt); 538 | self.kdf_opslimit = 524288; 539 | self.kdf_memlimit = 16777216; 540 | 541 | var stream: [104]u8 = undefined; 542 | defer crypto.secureZero(u8, &stream); 543 | 544 | const params = crypto.pwhash.scrypt.Params.fromLimits(self.kdf_opslimit, @intCast(self.kdf_memlimit)); 545 | try crypto.pwhash.scrypt.kdf(allocator, &stream, password, &self.kdf_salt, params); 546 | 547 | self.xorData(&stream); 548 | self.kdf_algorithm = "Sc".*; 549 | } 550 | 551 | pub fn toFile(self: *const SecretKey, path: []const u8) !void { 552 | const fd = try fs.cwd().createFile(path, .{ .exclusive = true, .mode = 0o600 }); 553 | defer fd.close(); 554 | 555 | var buf: [4096]u8 = undefined; 556 | var file_writer = fd.writer(&buf); 557 | const writer = &file_writer.interface; 558 | 559 | // Write untrusted comment 560 | try writer.writeAll(self.untrusted_comment); 561 | try writer.writeAll("\n"); 562 | 563 | // Encode secret key 564 | var bin: [158]u8 = undefined; 565 | @memcpy(bin[0..2], &self.signature_algorithm); 566 | @memcpy(bin[2..4], &self.kdf_algorithm); 567 | @memcpy(bin[4..6], &self.checksum_algorithm); 568 | @memcpy(bin[6..38], &self.kdf_salt); 569 | mem.writeInt(u64, bin[38..46], self.kdf_opslimit, Endian.little); 570 | mem.writeInt(u64, bin[46..54], self.kdf_memlimit, Endian.little); 571 | @memcpy(bin[54..62], &self.key_id); 572 | @memcpy(bin[62..126], &self.secret_key); 573 | @memcpy(bin[126..158], &self.checksum); 574 | 575 | const Base64Encoder = base64.standard.Encoder; 576 | var encoded: [Base64Encoder.calcSize(158)]u8 = undefined; 577 | _ = Base64Encoder.encode(&encoded, &bin); 578 | try writer.writeAll(&encoded); 579 | try writer.writeAll("\n"); 580 | 581 | try writer.flush(); 582 | } 583 | }; 584 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const clap = @import("clap.zig"); 2 | const std = @import("std"); 3 | const base64 = std.base64; 4 | const crypto = std.crypto; 5 | const fs = std.fs; 6 | const fmt = std.fmt; 7 | const heap = std.heap; 8 | const math = std.math; 9 | const mem = std.mem; 10 | const process = std.process; 11 | const Blake2b512 = crypto.hash.blake2.Blake2b512; 12 | const Ed25519 = crypto.sign.Ed25519; 13 | const Endian = std.builtin.Endian; 14 | 15 | const lib = @import("minizign"); 16 | const PublicKey = lib.PublicKey; 17 | const Signature = lib.Signature; 18 | const SecretKey = lib.SecretKey; 19 | 20 | fn verify(allocator: mem.Allocator, pks: []const PublicKey, path: []const u8, sig: Signature, prehash: ?bool) !void { 21 | var had_key_id_mismatch = false; 22 | var i: usize = pks.len; 23 | while (i > 0) { 24 | i -= 1; 25 | const fd = try fs.cwd().openFile(path, .{ .mode = .read_only }); 26 | defer fd.close(); 27 | if (pks[i].verifyFile(allocator, fd, sig, prehash)) |_| { 28 | return; 29 | } else |err| { 30 | if (err == error.KeyIdMismatch) { 31 | had_key_id_mismatch = true; 32 | } 33 | } 34 | } 35 | return if (had_key_id_mismatch) error.KeyIdMismatch else error.SignatureVerificationFailed; 36 | } 37 | 38 | fn sign(allocator: mem.Allocator, sk_path: []const u8, input_path: []const u8, sig_path: []const u8, trusted_comment: []const u8, untrusted_comment: []const u8) !void { 39 | // Load secret key 40 | var sk = try SecretKey.fromFile(allocator, sk_path); 41 | defer sk.deinit(); 42 | 43 | // Get password if key is encrypted 44 | if (mem.eql(u8, &sk.kdf_algorithm, "Sc")) { 45 | const password = try getPassword(allocator); 46 | defer allocator.free(password); 47 | try sk.decrypt(allocator, password); 48 | } 49 | 50 | // Open input file 51 | const fd = try fs.cwd().openFile(input_path, .{ .mode = .read_only }); 52 | defer fd.close(); 53 | 54 | // Sign the file (prehash mode by default) 55 | var signature = try sk.signFile(allocator, fd, true, trusted_comment); 56 | defer signature.deinit(); 57 | 58 | // Write signature to file 59 | try signature.toFile(sig_path, untrusted_comment); 60 | } 61 | 62 | fn generate(allocator: mem.Allocator, sk_path: []const u8, pk_path: []const u8, password: ?[]const u8) !void { 63 | // Generate new keypair 64 | var sk = try SecretKey.generate(allocator); 65 | defer sk.deinit(); 66 | 67 | // Extract public key BEFORE encryption 68 | const pk = sk.getPublicKey(); 69 | 70 | // Encrypt if password is provided 71 | if (password) |pwd| { 72 | try sk.encrypt(allocator, pwd); 73 | } 74 | 75 | // Save secret key and public key 76 | try sk.toFile(sk_path); 77 | try pk.toFile(pk_path); 78 | } 79 | 80 | fn getPassword(allocator: mem.Allocator) ![]u8 { 81 | const stdin = fs.File.stdin(); 82 | const stderr = fs.File.stderr(); 83 | 84 | const builtin = @import("builtin"); 85 | const has_termios = builtin.os.tag != .wasi; 86 | 87 | const is_terminal = if (has_termios) std.posix.isatty(stdin.handle) else false; 88 | 89 | var original: std.posix.termios = undefined; 90 | if (has_termios and is_terminal) { 91 | original = try std.posix.tcgetattr(stdin.handle); 92 | var termios = original; 93 | termios.lflag.ECHO = false; 94 | termios.lflag.ECHONL = false; 95 | try std.posix.tcsetattr(stdin.handle, .FLUSH, termios); 96 | try stderr.writeAll("Password: "); 97 | } 98 | defer if (has_termios and is_terminal) { 99 | stderr.writeAll("\n") catch {}; 100 | std.posix.tcsetattr(stdin.handle, .FLUSH, original) catch {}; 101 | }; 102 | 103 | var reader_buf: [1024]u8 = undefined; 104 | var reader = stdin.reader(&reader_buf); 105 | const line = try reader.interface.takeDelimiterExclusive('\n'); 106 | return allocator.dupe(u8, mem.trim(u8, line, &std.ascii.whitespace)); 107 | } 108 | 109 | const params = clap.parseParamsComptime( 110 | \\ -h, --help Display this help and exit 111 | \\ -p, --publickey-path Public key path to a file 112 | \\ -P, --publickey Public key, as a BASE64-encoded string 113 | \\ -s, --secretkey-path Secret key path to a file 114 | \\ -l, --legacy Accept legacy signatures 115 | \\ -m, --input Input file 116 | \\ -o, --output Output file (signature) 117 | \\ -q, --quiet Quiet mode 118 | \\ -V, --verify Verify 119 | \\ -S, --sign Sign 120 | \\ -G, --generate Generate a new key pair 121 | \\ -C, --convert Convert the given public key to SSH format 122 | \\ -t, --trusted-comment Trusted comment 123 | \\ -c, --untrusted-comment Untrusted comment 124 | ); 125 | 126 | fn usage() noreturn { 127 | var buf: [1024]u8 = undefined; 128 | var stderr_writer = std.fs.File.stderr().writer(&buf); 129 | const stderr = &stderr_writer.interface; 130 | stderr.writeAll("Usage:\n") catch unreachable; 131 | clap.help(stderr, clap.Help, ¶ms, .{}) catch unreachable; 132 | stderr.flush() catch unreachable; 133 | process.exit(1); 134 | } 135 | 136 | fn getDefaultSecretKeyPath(allocator: mem.Allocator) !?[]u8 { 137 | const builtin = @import("builtin"); 138 | 139 | // First check MINISIGN_CONFIG_DIR environment variable 140 | if (process.getEnvVarOwned(allocator, "MINISIGN_CONFIG_DIR")) |config_dir| { 141 | defer allocator.free(config_dir); 142 | const path = try fmt.allocPrint(allocator, "{s}{c}minisign.key", .{ config_dir, fs.path.sep }); 143 | return path; 144 | } else |_| {} 145 | 146 | // Try $HOME/.minisign/minisign.key 147 | if (process.getEnvVarOwned(allocator, "HOME")) |home| { 148 | defer allocator.free(home); 149 | const path = try fmt.allocPrint(allocator, "{s}{c}.minisign{c}minisign.key", .{ home, fs.path.sep, fs.path.sep }); 150 | // Check if file exists, if not continue to next option 151 | fs.cwd().access(path, .{}) catch { 152 | allocator.free(path); 153 | // File doesn't exist, try app data dir (not available on WASI) 154 | if (builtin.os.tag != .wasi) { 155 | if (fs.getAppDataDir(allocator, "minisign")) |app_dir| { 156 | defer allocator.free(app_dir); 157 | const app_path = try fmt.allocPrint(allocator, "{s}{c}minisign.key", .{ app_dir, fs.path.sep }); 158 | return app_path; 159 | } else |_| {} 160 | } 161 | return null; 162 | }; 163 | return path; 164 | } else |_| {} 165 | 166 | // Try app data directory (not available on WASI) 167 | if (builtin.os.tag != .wasi) { 168 | if (fs.getAppDataDir(allocator, "minisign")) |app_dir| { 169 | defer allocator.free(app_dir); 170 | const path = try fmt.allocPrint(allocator, "{s}{c}minisign.key", .{ app_dir, fs.path.sep }); 171 | return path; 172 | } else |_| {} 173 | } 174 | 175 | // No default available 176 | return null; 177 | } 178 | 179 | fn doit(gpa_allocator: mem.Allocator) !void { 180 | var diag = clap.Diagnostic{}; 181 | var res = clap.parse(clap.Help, ¶ms, .{ 182 | .PATH = clap.parsers.string, 183 | .STRING = clap.parsers.string, 184 | }, .{ 185 | .allocator = gpa_allocator, 186 | .diagnostic = &diag, 187 | }) catch |err| { 188 | var buf: [1024]u8 = undefined; 189 | var stderr_writer = std.fs.File.stderr().writer(&buf); 190 | const stderr = &stderr_writer.interface; 191 | diag.report(stderr, err) catch {}; 192 | stderr.flush() catch {}; 193 | process.exit(1); 194 | }; 195 | defer res.deinit(); 196 | 197 | if (res.args.help != 0) usage(); 198 | const quiet = res.args.quiet; 199 | const prehash: ?bool = if (res.args.legacy != 0) null else true; 200 | const pk_b64 = res.args.publickey; 201 | const pk_path = @field(res.args, "publickey-path"); 202 | const sk_path_arg = @field(res.args, "secretkey-path"); 203 | const input_path = res.args.input; 204 | const output_path = res.args.output; 205 | const sign_mode = res.args.sign != 0; 206 | const generate_mode = res.args.generate != 0; 207 | 208 | // Determine secret key path (from arg or default) 209 | const default_sk_path = try getDefaultSecretKeyPath(gpa_allocator); 210 | defer if (default_sk_path) |path| gpa_allocator.free(path); 211 | const sk_path = sk_path_arg orelse default_sk_path; 212 | 213 | // Handle key generation mode 214 | if (generate_mode) { 215 | if (sk_path == null) { 216 | var stderr_writer = fs.File.stderr().writer(&.{}); 217 | stderr_writer.interface.writeAll("Error: Secret key path (-s) is required for key generation\n") catch {}; 218 | usage(); 219 | } 220 | 221 | var arena = heap.ArenaAllocator.init(gpa_allocator); 222 | defer arena.deinit(); 223 | 224 | const public_key_path = if (pk_path) |path| path else blk: { 225 | break :blk try fmt.allocPrint(arena.allocator(), "{s}.pub", .{sk_path.?}); 226 | }; 227 | 228 | const sk_exists = if (fs.cwd().access(sk_path.?, .{})) true else |_| false; 229 | const pk_exists = if (fs.cwd().access(public_key_path, .{})) true else |_| false; 230 | 231 | if (sk_exists or pk_exists) { 232 | const stderr = fs.File.stderr(); 233 | const stdin = fs.File.stdin(); 234 | var stderr_writer = stderr.writer(&.{}); 235 | const writer = &stderr_writer.interface; 236 | 237 | if (sk_exists and pk_exists) { 238 | try writer.writeAll("Warning: Both key files already exist:\n"); 239 | try writer.print(" {s}\n", .{sk_path.?}); 240 | try writer.print(" {s}\n", .{public_key_path}); 241 | } else if (sk_exists) { 242 | try writer.print("Warning: Secret key file already exists: {s}\n", .{sk_path.?}); 243 | } else { 244 | try writer.print("Warning: Public key file already exists: {s}\n", .{public_key_path}); 245 | } 246 | 247 | try stderr.writeAll("Overwrite? (y/N): "); 248 | 249 | var response_buf: [10]u8 = undefined; 250 | var reader = stdin.reader(&response_buf); 251 | const response = reader.interface.takeDelimiterExclusive('\n') catch ""; 252 | 253 | const trimmed = mem.trim(u8, response, &std.ascii.whitespace); 254 | if (!mem.eql(u8, trimmed, "y") and !mem.eql(u8, trimmed, "Y")) { 255 | try writer.writeAll("Aborted.\n"); 256 | process.exit(1); 257 | } 258 | 259 | // Delete existing files if user confirmed 260 | if (sk_exists) { 261 | try fs.cwd().deleteFile(sk_path.?); 262 | } 263 | if (pk_exists) { 264 | try fs.cwd().deleteFile(public_key_path); 265 | } 266 | } 267 | 268 | // Prompt for password 269 | const stderr = fs.File.stderr(); 270 | try stderr.writeAll("Enter password (leave empty for unencrypted key): "); 271 | const password = try getPassword(arena.allocator()); 272 | defer arena.allocator().free(password); 273 | 274 | const pwd = if (password.len > 0) password else null; 275 | 276 | try generate(arena.allocator(), sk_path.?, public_key_path, pwd); 277 | 278 | if (quiet == 0) { 279 | var stdout_writer = fs.File.stdout().writer(&.{}); 280 | const writer = &stdout_writer.interface; 281 | try writer.print("Secret key written to {s}\n", .{sk_path.?}); 282 | try writer.print("Public key written to {s}\n", .{public_key_path}); 283 | } 284 | return; 285 | } 286 | 287 | // Handle signing mode 288 | if (sign_mode) { 289 | if (input_path == null) usage(); 290 | if (sk_path == null) { 291 | var stderr_writer = fs.File.stderr().writer(&.{}); 292 | stderr_writer.interface.writeAll("Error: Secret key path is required for signing\n") catch {}; 293 | usage(); 294 | } 295 | 296 | var arena = heap.ArenaAllocator.init(gpa_allocator); 297 | defer arena.deinit(); 298 | 299 | const sig_path = if (output_path) |path| path else blk: { 300 | break :blk try fmt.allocPrint(arena.allocator(), "{s}.minisig", .{input_path.?}); 301 | }; 302 | 303 | const trusted_comment = if (@field(res.args, "trusted-comment")) |tc| tc else blk: { 304 | const timestamp = std.time.timestamp(); 305 | break :blk try fmt.allocPrint(arena.allocator(), "timestamp:{d}", .{timestamp}); 306 | }; 307 | 308 | const untrusted_comment = if (@field(res.args, "untrusted-comment")) |uc| uc else "signature from minizign secret key"; 309 | 310 | try sign(arena.allocator(), sk_path.?, input_path.?, sig_path, trusted_comment, untrusted_comment); 311 | 312 | if (quiet == 0) { 313 | var stdout_writer = fs.File.stdout().writer(&.{}); 314 | try stdout_writer.interface.print("Signature written to {s}\n", .{sig_path}); 315 | } 316 | return; 317 | } 318 | 319 | // Handle conversion mode 320 | if (pk_path == null and pk_b64 == null) { 321 | usage(); 322 | } 323 | var pks_buf: [64]PublicKey = undefined; 324 | const pks = if (pk_b64) |b64| blk: { 325 | pks_buf[0] = try PublicKey.decodeFromBase64(b64); 326 | break :blk pks_buf[0..1]; 327 | } else try PublicKey.fromFile(gpa_allocator, &pks_buf, pk_path.?); 328 | 329 | if (res.args.convert != 0) { 330 | const ssh_key = pks[0].getSshKey(); 331 | const fd = std.fs.File.stdout(); 332 | _ = try fd.write(&ssh_key); 333 | return; 334 | } 335 | 336 | // Handle verification mode 337 | if (input_path == null) { 338 | usage(); 339 | } 340 | var arena = heap.ArenaAllocator.init(gpa_allocator); 341 | defer arena.deinit(); 342 | const sig_path = if (output_path) |path| path else try fmt.allocPrint(arena.allocator(), "{s}.minisig", .{input_path.?}); 343 | const sig = try Signature.fromFile(arena.allocator(), sig_path); 344 | if (verify(arena.allocator(), pks, input_path.?, sig, prehash)) { 345 | if (quiet == 0) { 346 | var stdout_writer = fs.File.stdout().writer(&.{}); 347 | try stdout_writer.interface.print("Signature and comment signature verified\nTrusted comment: {s}\n", .{sig.trusted_comment}); 348 | } 349 | } else |err| { 350 | if (quiet == 0) { 351 | var stderr_writer = fs.File.stderr().writer(&.{}); 352 | const writer = &stderr_writer.interface; 353 | 354 | if (err == error.KeyIdMismatch) { 355 | writer.writeAll("Signature verification failed: key ID mismatch\n") catch {}; 356 | 357 | const sig_key_id = mem.readInt(u64, &sig.key_id, Endian.little); 358 | writer.print("Signature key ID: {X:0>16}\n", .{sig_key_id}) catch {}; 359 | 360 | const null_key_id: [8]u8 = @splat(0); 361 | const prefix = if (pks.len == 1) "Public key ID: " else "Public key IDs:\n "; 362 | writer.writeAll(prefix) catch {}; 363 | 364 | for (pks, 0..) |pk, i| { 365 | if (i > 0) writer.writeAll("\n ") catch {}; 366 | if (mem.eql(u8, &pk.key_id, &null_key_id)) { 367 | writer.writeAll("(not set)") catch {}; 368 | } else { 369 | const pk_key_id = mem.readInt(u64, &pk.key_id, Endian.little); 370 | writer.print("{X:0>16}", .{pk_key_id}) catch {}; 371 | } 372 | } 373 | writer.writeAll("\n") catch {}; 374 | } else { 375 | writer.writeAll("Signature verification failed\n") catch {}; 376 | } 377 | } 378 | process.exit(1); 379 | } 380 | } 381 | 382 | pub fn main() !void { 383 | var gpa = heap.DebugAllocator(.{}){}; 384 | defer _ = gpa.deinit(); 385 | try doit(gpa.allocator()); 386 | } 387 | -------------------------------------------------------------------------------- /src/wasm.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const lib = @import("minizign"); 3 | const PublicKey = lib.PublicKey; 4 | const Signature = lib.Signature; 5 | const Verifier = lib.Verifier; 6 | 7 | const alloc = std.heap.wasm_allocator; 8 | 9 | pub const Result = enum(isize) { 10 | OutOfMemory = -1, 11 | InvalidEncoding = -2, 12 | InvalidCharacter = -3, 13 | InvalidPadding = -4, 14 | NoSpaceLeft = -5, 15 | UnsupportedAlgorithm = -6, 16 | KeyIdMismatch = -7, 17 | SignatureVerificationFailed = -8, 18 | NonCanonical = -9, 19 | IdentityElement = -10, 20 | WeakPublicKey = -11, 21 | Overflow = -12, 22 | _, 23 | 24 | // Assert that none of the error code values are positive. 25 | comptime { 26 | const type_info = @typeInfo(Result); 27 | for (type_info.@"enum".fields) |field| { 28 | if (field.value >= 0) { 29 | @compileError("Result values must be negative."); 30 | } 31 | } 32 | } 33 | 34 | /// Given a pointer, convert it first to an integer, and then to the 35 | /// Result enum type. Asserts the highest bit is *not* set. 36 | fn fromPointer(ptr: *anyopaque) Result { 37 | const int: usize = @intFromPtr(ptr); 38 | 39 | // Assert that the first bit is not set since that would 40 | // make it a negative value. 41 | std.debug.assert(@clz(int) >= 1); 42 | 43 | return @enumFromInt(int); 44 | } 45 | }; 46 | 47 | /// Allocate a buffer in wasm memory 48 | export fn allocate(len: u32) Result { 49 | const buf = alloc.alloc(u8, len) catch |e| switch (e) { 50 | error.OutOfMemory => return .OutOfMemory, 51 | }; 52 | return Result.fromPointer(buf.ptr); 53 | } 54 | 55 | /// Free a buffer in wasm memory 56 | export fn free(pointer: [*]u8, len: u32) void { 57 | alloc.free(pointer[0..len]); 58 | } 59 | 60 | /// Takes minisign signature and creates a Signature object in memory. 61 | /// On success, returns the number of bytes used. On failure, returns 0. 62 | export fn signatureDecode(str: [*]const u8, len: u32) Result { 63 | const sig = struct { 64 | fn impl(str_: [*]const u8, len_: u32) !*Signature { 65 | const sig: *Signature = try alloc.create(Signature); 66 | errdefer alloc.destroy(sig); 67 | 68 | sig.* = try Signature.decode(alloc, str_[0..len_]); 69 | 70 | return sig; 71 | } 72 | }.impl(str, len) catch |e| switch (e) { 73 | error.OutOfMemory => return .OutOfMemory, 74 | error.InvalidEncoding => return .InvalidEncoding, 75 | error.InvalidCharacter => return .InvalidCharacter, 76 | error.InvalidPadding => return .InvalidPadding, 77 | error.NoSpaceLeft => return .NoSpaceLeft, 78 | }; 79 | return Result.fromPointer(sig); 80 | } 81 | 82 | /// Returns the pointer to the signatures trusted comment. 83 | export fn signatureGetTrustedComment(sig: *const Signature) [*]const u8 { 84 | return sig.trusted_comment.ptr; 85 | } 86 | 87 | /// Returns the length of the signatures trusted comment. 88 | export fn signatureGetTrustedCommentLength(sig: *const Signature) usize { 89 | return sig.trusted_comment.len; 90 | } 91 | 92 | /// De-initializes a signature object from a call to signatureDecode. 93 | export fn signatureDeinit(sig: *Signature) void { 94 | sig.deinit(); 95 | } 96 | 97 | /// Takes a base64 encoded string and creates a PublicKey object in the provided buffer. 98 | /// On success, returns the number of bytes used. On failure, returns 0. 99 | export fn publicKeyDecodeFromBase64(str: [*]const u8, len: u32) Result { 100 | const pk = struct { 101 | fn impl(str_: [*]const u8, len_: u32) !*PublicKey { 102 | const pk: *PublicKey = try alloc.create(PublicKey); 103 | errdefer alloc.destroy(pk); 104 | 105 | pk.* = try PublicKey.decodeFromBase64(str_[0..len_]); 106 | 107 | return pk; 108 | } 109 | }.impl(str, len) catch |e| switch (e) { 110 | error.OutOfMemory => return .OutOfMemory, 111 | error.InvalidEncoding => return .InvalidEncoding, 112 | error.InvalidCharacter => return .InvalidCharacter, 113 | error.InvalidPadding => return .InvalidPadding, 114 | error.NoSpaceLeft => return .NoSpaceLeft, 115 | error.UnsupportedAlgorithm => return .UnsupportedAlgorithm, 116 | }; 117 | 118 | return Result.fromPointer(pk); 119 | } 120 | 121 | /// Initialize a list of public keys from an ssh encoded file. 122 | /// Returns the number of keys decoded or an error code. 123 | export fn publicKeyDecodeFromSsh( 124 | pks: [*]PublicKey, 125 | pksLength: usize, 126 | lines: [*]const u8, 127 | linesLength: usize, 128 | ) Result { 129 | const result = PublicKey.decodeFromSsh(pks[0..pksLength], lines[0..linesLength]) catch |e| switch (e) { 130 | error.InvalidEncoding => return .InvalidEncoding, 131 | error.InvalidCharacter => return .InvalidCharacter, 132 | error.InvalidPadding => return .InvalidPadding, 133 | error.NoSpaceLeft => return .NoSpaceLeft, 134 | error.Overflow => return .Overflow, 135 | }; 136 | return @enumFromInt(result.len); 137 | } 138 | 139 | /// De-initialize a public key object from a call to any publicKeyDecode* function. 140 | export fn publicKeyDeinit(pk: *PublicKey) void { 141 | alloc.destroy(pk); 142 | } 143 | 144 | /// Creates an incremental Verifier struct from the given public key. 145 | /// Returns a pointer to the struct or an error code. 146 | export fn publicKeyVerifier(pk: *const PublicKey, sig: *const Signature) Result { 147 | const verifier = struct { 148 | fn impl(pk_: *const PublicKey, sig_: *const Signature) !*Verifier { 149 | const verifier: *Verifier = try alloc.create(Verifier); 150 | errdefer alloc.destroy(verifier); 151 | 152 | verifier.* = try pk_.verifier(sig_); 153 | 154 | return verifier; 155 | } 156 | }.impl(pk, sig) catch |e| switch (e) { 157 | error.OutOfMemory => return .OutOfMemory, 158 | error.InvalidEncoding => return .InvalidEncoding, 159 | error.KeyIdMismatch => return .KeyIdMismatch, 160 | error.UnsupportedAlgorithm => return .UnsupportedAlgorithm, 161 | error.NonCanonical => return .NonCanonical, 162 | error.IdentityElement => return .IdentityElement, 163 | }; 164 | 165 | return Result.fromPointer(verifier); 166 | } 167 | 168 | /// Add bytes to by verified. 169 | export fn verifierUpdate(verifier: *Verifier, bytes: [*]const u8, length: u32) void { 170 | verifier.update(bytes[0..length]); 171 | } 172 | 173 | /// Finalizes the hash over bytes previously passed to the verifier through 174 | /// calls to verifierUpdate and returns a Result value. If negative, an error 175 | /// has occurred and the file should not be trusted. Otherwise, the result 176 | /// should be the value 1. 177 | export fn verifierVerify(verifier: *Verifier) Result { 178 | verifier.verify(alloc) catch |e| switch (e) { 179 | error.OutOfMemory => return .OutOfMemory, 180 | error.InvalidEncoding => return .InvalidEncoding, 181 | error.SignatureVerificationFailed => return .SignatureVerificationFailed, 182 | error.NonCanonical => return .NonCanonical, 183 | error.IdentityElement => return .IdentityElement, 184 | error.WeakPublicKey => return .WeakPublicKey, 185 | }; 186 | return @enumFromInt(1); 187 | } 188 | 189 | /// De-initialize a verifier struct from a call to publicKeyVerifier 190 | export fn verifierDeinit(verifier: *Verifier) void { 191 | alloc.destroy(verifier); 192 | } 193 | -------------------------------------------------------------------------------- /test_compatibility.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Colors for output 6 | RED='\033[0;31m' 7 | GREEN='\033[0;32m' 8 | YELLOW='\033[1;33m' 9 | NC='\033[0m' # No Color 10 | 11 | # Test counter 12 | TESTS_RUN=0 13 | TESTS_PASSED=0 14 | TESTS_FAILED=0 15 | 16 | # Paths (can be overridden by environment variables) 17 | MINISIGN="${MINISIGN:-minisign}" 18 | MINIZIGN="${MINIZIGN:-./zig-out/bin/minizign}" 19 | TEST_DIR="./tmp/compat_test" 20 | 21 | # Check binaries exist 22 | if ! command -v "$MINISIGN" >/dev/null 2>&1; then 23 | echo -e "${RED}Error: minisign not found${NC}" 24 | echo "Install minisign or set MINISIGN environment variable" 25 | exit 1 26 | fi 27 | 28 | if ! command -v "$MINIZIGN" >/dev/null 2>&1; then 29 | echo -e "${RED}Error: minizign not found${NC}" 30 | echo "Run 'zig build' first or set MINIZIGN environment variable" 31 | exit 1 32 | fi 33 | 34 | # Helper functions 35 | log_test() { 36 | echo -e "${YELLOW}TEST $((TESTS_RUN + 1)): $1${NC}" 37 | TESTS_RUN=$((TESTS_RUN + 1)) 38 | } 39 | 40 | log_pass() { 41 | echo -e "${GREEN}✓ PASS${NC}" 42 | TESTS_PASSED=$((TESTS_PASSED + 1)) 43 | } 44 | 45 | log_fail() { 46 | echo -e "${RED}✗ FAIL: $1${NC}" 47 | TESTS_FAILED=$((TESTS_FAILED + 1)) 48 | } 49 | 50 | cleanup() { 51 | rm -rf "$TEST_DIR" 52 | } 53 | 54 | setup() { 55 | cleanup 56 | mkdir -p "$TEST_DIR" 57 | echo "Test data for minisign compatibility" > "$TEST_DIR/test.txt" 58 | } 59 | 60 | # Trap to cleanup on exit 61 | trap cleanup EXIT 62 | 63 | echo "=== Minisign <-> Minizign Compatibility Test ===" 64 | echo "minisign: $($MINISIGN -v 2>&1 | head -n1 || echo 'unknown')" 65 | echo "minizign: Zig implementation" 66 | echo "" 67 | 68 | setup 69 | 70 | # Test 1: Generate with minisign, sign with minisign, verify with minizign 71 | log_test "Generate(C) → Sign(C) → Verify(Zig)" 72 | printf "\n\n" | $MINISIGN -G -p "$TEST_DIR/test1.pub" -s "$TEST_DIR/test1.key" >/dev/null 2>&1 73 | printf "\n" | $MINISIGN -S -s "$TEST_DIR/test1.key" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test1.txt.minisig" >/dev/null 2>&1 74 | if $MINIZIGN -V -p "$TEST_DIR/test1.pub" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test1.txt.minisig" >/dev/null 2>&1; then 75 | log_pass 76 | else 77 | log_fail "Verification failed" 78 | fi 79 | 80 | # Test 2: Generate with minizign, sign with minizign, verify with minisign 81 | log_test "Generate(Zig) → Sign(Zig) → Verify(C)" 82 | printf "\n" | $MINIZIGN -G -s "$TEST_DIR/test2.key" -p "$TEST_DIR/test2.pub" >/dev/null 2>&1 83 | printf "\n" | $MINIZIGN -S -s "$TEST_DIR/test2.key" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test2.txt.minisig" >/dev/null 2>&1 84 | if $MINISIGN -V -p "$TEST_DIR/test2.pub" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test2.txt.minisig" >/dev/null 2>&1; then 85 | log_pass 86 | else 87 | log_fail "Verification failed" 88 | fi 89 | 90 | # Test 3: Generate with minisign, sign with minizign, verify with minisign 91 | log_test "Generate(C) → Sign(Zig) → Verify(C)" 92 | printf "\n\n" | $MINISIGN -G -p "$TEST_DIR/test3.pub" -s "$TEST_DIR/test3.key" >/dev/null 2>&1 93 | printf "\n" | $MINIZIGN -S -s "$TEST_DIR/test3.key" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test3.txt.minisig" >/dev/null 2>&1 94 | if $MINISIGN -V -p "$TEST_DIR/test3.pub" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test3.txt.minisig" >/dev/null 2>&1; then 95 | log_pass 96 | else 97 | log_fail "Verification failed" 98 | fi 99 | 100 | # Test 4: Generate with minizign, sign with minisign, verify with minizign 101 | log_test "Generate(Zig) → Sign(C) → Verify(Zig)" 102 | printf "\n" | $MINIZIGN -G -s "$TEST_DIR/test4.key" -p "$TEST_DIR/test4.pub" >/dev/null 2>&1 103 | printf "\n" | $MINISIGN -S -s "$TEST_DIR/test4.key" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test4.txt.minisig" >/dev/null 2>&1 104 | if $MINIZIGN -V -p "$TEST_DIR/test4.pub" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test4.txt.minisig" >/dev/null 2>&1; then 105 | log_pass 106 | else 107 | log_fail "Verification failed" 108 | fi 109 | 110 | # Test 5: Generate with minisign (with password), sign and verify cross-implementation 111 | log_test "Generate(C, encrypted) → Sign(Zig) → Verify(C)" 112 | printf "testpass\ntestpass\n" | $MINISIGN -G -p "$TEST_DIR/test5.pub" -s "$TEST_DIR/test5.key" >/dev/null 2>&1 113 | echo "testpass" | $MINIZIGN -S -s "$TEST_DIR/test5.key" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test5.txt.minisig" >/dev/null 2>&1 114 | if $MINISIGN -V -p "$TEST_DIR/test5.pub" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test5.txt.minisig" >/dev/null 2>&1; then 115 | log_pass 116 | else 117 | log_fail "Verification failed" 118 | fi 119 | 120 | # Test 6: Generate with minizign (with password), sign and verify cross-implementation 121 | log_test "Generate(Zig, encrypted) → Sign(C) → Verify(Zig)" 122 | echo "testpass" | $MINIZIGN -G -s "$TEST_DIR/test6.key" -p "$TEST_DIR/test6.pub" >/dev/null 2>&1 123 | printf "testpass\n" | $MINISIGN -S -s "$TEST_DIR/test6.key" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test6.txt.minisig" >/dev/null 2>&1 124 | if $MINIZIGN -V -p "$TEST_DIR/test6.pub" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test6.txt.minisig" >/dev/null 2>&1; then 125 | log_pass 126 | else 127 | log_fail "Verification failed" 128 | fi 129 | 130 | # Test 7: Verify public key format compatibility 131 | log_test "Public key format compatibility" 132 | printf "\n\n" | $MINISIGN -G -p "$TEST_DIR/test7a.pub" -s "$TEST_DIR/test7a.key" >/dev/null 2>&1 133 | printf "\n" | $MINIZIGN -G -s "$TEST_DIR/test7b.key" -p "$TEST_DIR/test7b.pub" >/dev/null 2>&1 134 | 135 | # Both should be able to read each other's public keys 136 | if [ -f "$TEST_DIR/test7a.pub" ] && [ -f "$TEST_DIR/test7b.pub" ]; then 137 | # Check that both public keys have the same structure 138 | C_LINES=$(wc -l < "$TEST_DIR/test7a.pub") 139 | ZIG_LINES=$(wc -l < "$TEST_DIR/test7b.pub") 140 | if [ "$C_LINES" -eq "$ZIG_LINES" ]; then 141 | log_pass 142 | else 143 | log_fail "Public key format differs: C=$C_LINES lines, Zig=$ZIG_LINES lines" 144 | fi 145 | else 146 | log_fail "Public key files not created" 147 | fi 148 | 149 | # Test 8: Verify signature format compatibility 150 | log_test "Signature format compatibility" 151 | printf "\n\n" | $MINISIGN -G -p "$TEST_DIR/test8.pub" -s "$TEST_DIR/test8.key" >/dev/null 2>&1 152 | printf "\n" | $MINISIGN -S -s "$TEST_DIR/test8.key" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test8a.txt.minisig" >/dev/null 2>&1 153 | printf "\n" | $MINIZIGN -S -s "$TEST_DIR/test8.key" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test8b.txt.minisig" >/dev/null 2>&1 154 | 155 | # Both should be able to verify each other's signatures 156 | C_VERIFY=0 157 | ZIG_VERIFY=0 158 | 159 | if $MINIZIGN -V -p "$TEST_DIR/test8.pub" -m "$TEST_DIR/test.txt" -o "$TEST_DIR/test8a.txt.minisig" >/dev/null 2>&1; then 160 | C_VERIFY=1 161 | fi 162 | 163 | if $MINISIGN -V -p "$TEST_DIR/test8.pub" -m "$TEST_DIR/test.txt" -x "$TEST_DIR/test8b.txt.minisig" >/dev/null 2>&1; then 164 | ZIG_VERIFY=1 165 | fi 166 | 167 | if [ "$C_VERIFY" -eq 1 ] && [ "$ZIG_VERIFY" -eq 1 ]; then 168 | log_pass 169 | else 170 | log_fail "Cross-verification failed (C_sig→Zig: $C_VERIFY, Zig_sig→C: $ZIG_VERIFY)" 171 | fi 172 | 173 | # Summary 174 | echo "" 175 | echo "=== Test Summary ===" 176 | echo "Tests run: $TESTS_RUN" 177 | echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}" 178 | if [ "$TESTS_FAILED" -gt 0 ]; then 179 | echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}" 180 | exit 1 181 | else 182 | echo -e "Tests failed: $TESTS_FAILED" 183 | echo -e "${GREEN}All tests passed!${NC}" 184 | exit 0 185 | fi 186 | --------------------------------------------------------------------------------