diff --git a/src/cli.zig b/src/cli.zig index aec7d53..114c000 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -94,10 +94,20 @@ pub const Input = struct { self.stdin = null; return null; }; - const trimmed = std.mem.trim(u8, line, " \t\r\n"); - // Blank lines separate records, they are not records. - if (trimmed.len == 0) continue; - return .{ .line = trimmed }; + // Only the line ending comes off, never the reader's own bytes. + // + // This used to trim spaces and tabs from both ends of every record, + // which is harmless for a code or a JSON object and silent data + // loss for a message: `printf ' hello '` piped into + // `deed encrypt` encrypted `hello`, and nothing said so. A record + // is whatever sat between two newlines, and what to do about the + // whitespace in it is the verb's business, not this one's. + const body = if (std.mem.endsWith(u8, line, "\r")) line[0 .. line.len - 1] else line; + // A truly empty line separates records and is not one. A line of + // spaces IS one, because for `deed encrypt` it is a message + // somebody chose to send. + if (body.len == 0) continue; + return .{ .line = body }; } } }; @@ -114,6 +124,45 @@ test "no positionals and no stdin yields nothing" { try std.testing.expect((try input.next()) == null); } +test "a record is handed back as it was written" { + // `deed encrypt` takes a record as the MESSAGE. This used to trim spaces and + // tabs off both ends of every record, so `printf ' hello '` piped in + // encrypted `hello` and nothing said so: the reader's own words, altered on + // the way to being sealed. + var source: std.Io.Reader = .fixed(" hello \nplain\n\ttabbed\t\n"); + var window: [64]u8 = undefined; + var limited = source.limited(.unlimited, &window); + var input = Input.init(&.{}, &limited.interface); + + try std.testing.expectEqualStrings(" hello ", (try input.next()).?.line); + try std.testing.expectEqualStrings("plain", (try input.next()).?.line); + try std.testing.expectEqualStrings("\ttabbed\t", (try input.next()).?.line); + try std.testing.expect((try input.next()) == null); +} + +test "a line ending comes off, and an empty line is still a separator" { + // CRLF is the one thing that is not the reader's bytes: it is how the line + // ended, not part of what they wrote. + var source: std.Io.Reader = .fixed("one\r\n\ntwo\r\n"); + var window: [64]u8 = undefined; + var limited = source.limited(.unlimited, &window); + var input = Input.init(&.{}, &limited.interface); + + try std.testing.expectEqualStrings("one", (try input.next()).?.line); + try std.testing.expectEqualStrings("two", (try input.next()).?.line); + try std.testing.expect((try input.next()) == null); +} + +test "a line of spaces is a record, not a separator" { + // For `deed encrypt` it is a message somebody chose to send. Only a truly + // empty line separates. + var source: std.Io.Reader = .fixed(" \n"); + var window: [64]u8 = undefined; + var limited = source.limited(.unlimited, &window); + var input = Input.init(&.{}, &limited.interface); + try std.testing.expectEqualStrings(" ", (try input.next()).?.line); +} + test "a record too long to hold does not take the stream with it" { // A 32-byte window over input whose second record is far longer than it. const data = "aa\n" ++ ("x" ** 200) ++ "\nbb\ncc\n"; diff --git a/src/cmd_crypt.zig b/src/cmd_crypt.zig index 3044ba3..1552738 100644 --- a/src/cmd_crypt.zig +++ b/src/cmd_crypt.zig @@ -98,6 +98,15 @@ pub fn run( }); return cli.exit_ok; } + // Everything after `--` is a message, whatever it starts with. Without + // this there was no way to encrypt a line beginning with a hyphen: it + // came back as an unknown option, which is a refusal to carry somebody's + // words on account of their first character. + if (std.mem.eql(u8, a, "--")) { + i += 1; + while (i < args.len) : (i += 1) try positionals.append(gpa, args[i]); + break; + } if (std.mem.startsWith(u8, a, "-")) { const is_sec = std.mem.eql(u8, a, "--sec"); const is_peer = std.mem.eql(u8, a, dir.peerFlag()); diff --git a/src/cmd_decode.zig b/src/cmd_decode.zig index 74c2810..b762a98 100644 --- a/src/cmd_decode.zig +++ b/src/cmd_decode.zig @@ -63,7 +63,9 @@ pub fn run( var failures: usize = 0; while (try input.next()) |record| { const code = switch (record) { - .line => |l| l, + // Trimmed here rather than in `Input`: surrounding space around + // a code is noise, and `deed encrypt` needs the same bytes it was given. + .line => |l| std.mem.trim(u8, l, " \t"), .too_long => { try err.print( "deed decode: skipped a code longer than {d} bytes\n", diff --git a/src/cmd_event.zig b/src/cmd_event.zig index 8fd5f53..7d6f8e2 100644 --- a/src/cmd_event.zig +++ b/src/cmd_event.zig @@ -171,7 +171,9 @@ pub fn run( var failures: usize = 0; while (try input.next()) |record| { const json = switch (record) { - .line => |l| l, + // Trimmed here rather than in `Input`: surrounding space around + // a draft is noise, and `deed encrypt` needs the same bytes it was given. + .line => |l| std.mem.trim(u8, l, " \t"), .too_long => { try err.print( "deed event: skipped a draft longer than {d} bytes\n", diff --git a/src/cmd_key.zig b/src/cmd_key.zig index 4abfd4e..168ad8a 100644 --- a/src/cmd_key.zig +++ b/src/cmd_key.zig @@ -128,7 +128,14 @@ fn public( var signer = keys.Signer.init(); defer signer.deinit(); - const kp = try signer.keyPairFromSecretKey(sk); + // Caught rather than propagated. 64 hex characters can still be a number + // outside the curve's range, and letting that escape printed `deed: + // InvalidSecretKey`, which names the branch the code took rather than + // telling the reader what is wrong with what they pasted. + const kp = signer.keyPairFromSecretKey(sk) catch { + try err.writeAll("deed key public: that is not a usable secret key (the number is outside the curve's range)\n"); + return cli.exit_fail; + }; const text = if (as_hex) try hex.encode(gpa, &kp.public_key) @@ -263,6 +270,19 @@ test "help is printed on stdout and succeeds" { try std.testing.expectEqualStrings("", r.err); } +test "a key outside the curve's range is explained, not named" { + // 64 hex characters can still be a number the curve cannot use. Letting it + // escape printed `deed: InvalidSecretKey`, which names the branch the code + // took rather than telling the reader what is wrong with what they pasted. + var ob: [1024]u8 = undefined; + var eb: [1024]u8 = undefined; + const r = try runKey(&.{ "public", "f" ** 64 }, &ob, &eb); + try std.testing.expectEqual(cli.exit_fail, r.code); + try std.testing.expectEqualStrings("", r.out); + try std.testing.expect(std.mem.indexOf(u8, r.err, "InvalidSecretKey") == null); + try std.testing.expect(std.mem.indexOf(u8, r.err, "curve") != null); +} + test "the subcommands answer --help too" { // They used to reject it as an unknown option, because help was only caught // at the subcommand position and never inside the argument loops. diff --git a/src/cmd_verify.zig b/src/cmd_verify.zig index b9c4796..8904a77 100644 --- a/src/cmd_verify.zig +++ b/src/cmd_verify.zig @@ -71,7 +71,9 @@ pub fn run( var failures: usize = 0; while (try input.next()) |record| { const json = switch (record) { - .line => |l| l, + // Trimmed here rather than in `Input`: surrounding space around + // an event is noise, and `deed encrypt` needs the same bytes it was given. + .line => |l| std.mem.trim(u8, l, " \t"), .too_long => { try err.print( "deed verify: skipped an event longer than {d} bytes\n",