// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! What the parsers must do with input nobody wrote. //! //! Everything here is a property rather than an example: not "this name //! parses to those flags" — the unit tests have those — but "whatever //! arrives, the parser terminates, stays inside its buffers, and if it claims //! to have understood the input then writing it back out and reading it again //! gives the same answer". //! //! Stability is the property that earns its keep here, and it is a stronger //! claim than it sounds. A maildir library rewrites names constantly: every //! flag change is a `parse`, a change, and a `write`. If that round trip is //! not a fixed point — if writing a parsed name can produce something that //! parses differently — then a message's name drifts a little every time //! anybody touches it, and since the name is the message's identity, the //! message eventually becomes a different message. The name target therefore //! asks for the *second* write to equal the first rather than for the output //! to equal the input: a name may legitimately be repaired on the way in //! (unsorted flags get sorted, a duplicate letter is dropped), but repairing //! it twice must change nothing, and the repair must not touch `base`. //! //! Each target is an ordinary test as well as a fuzz target. Without `--fuzz` //! it runs the seeds beside it, so `zig build test` exercises the same //! properties on input that has already been interesting once. //! //! Note that Zig 0.16.0 cannot build a test executable in fuzz mode without a //! patched standard library, and leaves the fuzzer's coverage table empty even //! then; `flake.nix` says what the patch is and `tools/fuzz.zig` is the loop //! that stands in for the fuzzer. The properties are worth having either way. const builtin = @import("builtin"); const std = @import("std"); const Io = std.Io; const Dir = Io.Dir; const Allocator = std.mem.Allocator; const testing = std.testing; const maildir = @import("maildir"); const Flags = maildir.Flags; const Name = maildir.Name; const folder = maildir.folder; const quota = maildir.quota; /// The allocator the targets run against. /// /// Under `zig build test` that is the testing allocator, which reports a leak /// as a failure. `tools/fuzz.zig` cannot name it — it is not a test build — so /// it sets this to a checked allocator of its own instead. Nothing here /// allocates yet; it is part of the contract the driver expects. pub var backing: Allocator = if (builtin.is_test) testing.allocator else undefined; /// One fuzz target: what to call it, what it already knows to be interesting, /// how much of an input it can read, and the property itself. pub const Target = struct { name: []const u8, corpus: []const []const u8, /// The size of the buffer the target reads its slice into. /// /// `Smith.slice` answers a length larger than its buffer with an *empty* /// slice rather than a truncated one, so a generator that does not know /// this number will silently hand the target nothing at all. content_max: usize, run: *const fn (input: []const u8) anyerror!void, }; pub const all = [_]Target{ .{ .name = "name", .corpus = &name_seeds, .content_max = name_max, .run = runName }, .{ .name = "flags", .corpus = &flag_seeds, .content_max = flag_max, .run = runFlags }, .{ .name = "folder", .corpus = &folder_seeds, .content_max = folder_max, .run = runFolder }, .{ .name = "quota", .corpus = "a_seeds, .content_max = quota_max, .run = runQuota }, }; // -- message names ------------------------------------------------------------ const name_max = Dir.max_name_bytes; const name_seeds = [_][]const u8{ "1757700000.M492817R3f0a1c2b4d5e6f70Q1.mail.example.com", "1757700000.M492817R3f0a1c2b4d5e6f70Q1.mail.example.com:2,RS", "1757700000.M1P2Q3.host,S=4211,W=4300:2,DFPRST", "1757700000.M1P2Q3.host:2,", "1757700000.M1P2Q3.host:2,TSRPFD", // flags out of order "1757700000.M1P2Q3.host:2,SS", // the same flag twice "1757700000.M1P2Q3.host:2,Sab", // Dovecot keywords "1757700000.M1P2Q3.host:1,experimental", "1757700000.M1P2Q3.host:2,S=1", // an info field that is not flags "a:b:2,S", // a colon in what would have to be the unique part "x,S=:2,S", // an empty size field "x,S=99999999999999999999999:2,S", // a size that does not fit ":2,S", ":", "", }; /// A name that has been parsed and written once must not change if it is /// parsed and written again, and the repair must not alter what identifies the /// message. fn nameProperty(input: []const u8) !void { const first: Name = .parse(input, ':'); var once_buffer: [name_max * 2]u8 = undefined; const once = first.bufWrite(&once_buffer, ':') catch return; const second: Name = .parse(once, ':'); var twice_buffer: [name_max * 2]u8 = undefined; const twice = try second.bufWrite(&twice_buffer, ':'); // The fixed point: repairing a name twice changes nothing. try testing.expectEqualStrings(once, twice); // The identity is untouched by the repair. This is the one that would // lose mail: a message whose base changed is, to every other program // sharing the maildir, a different message. try testing.expectEqualStrings(first.base(), second.base()); try testing.expectEqualStrings(first.unique, second.unique); // And so are the flags, and the size the name claims. try testing.expect(first.flags().eql(second.flags())); try testing.expectEqual(first.size(), second.size()); try testing.expectEqual(first.virtualSize(), second.virtualSize()); // A name always begins with its own unique part, and the unique part // always begins with the base. Nothing may be inserted before them. try testing.expect(std.mem.startsWith(u8, once, first.unique)); try testing.expect(std.mem.startsWith(u8, first.unique, first.base())); } test "fuzz names" { for (name_seeds) |seed| try nameProperty(seed); try testing.fuzz({}, fuzzName, .{}); } fn fuzzName(_: void, smith: *testing.Smith) !void { var buffer: [name_max]u8 = undefined; try nameProperty(buffer[0..smith.slice(&buffer)]); } fn runName(input: []const u8) anyerror!void { var smith: testing.Smith = .{ .in = input }; var buffer: [name_max]u8 = undefined; return nameProperty(buffer[0..smith.slice(&buffer)]); } // -- flags -------------------------------------------------------------------- const flag_max = 256; const flag_seeds = [_][]const u8{ "", "S", "DFPRST", "TSRPFD", "SSSS", "Sab", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", "S,", "2,S", "\x00", }; /// Flags that parse must be written sorted, without duplicates, with exactly /// the letters that went in, and must read back as the same set. fn flagsProperty(input: []const u8) !void { const parsed = Flags.parse(input) catch |err| { // The only refusal is a character that is not a letter, and there has // to be one in the input for it to happen. try testing.expectEqual(error.InvalidFlag, err); for (input) |c| if (Flags.Letters.indexOf(c) == null) return; return error.RefusedValidFlags; }; var buffer: [64]u8 = undefined; var w: Io.Writer = .fixed(&buffer); try parsed.format(&w); const text = w.buffered(); // Strictly ascending, which is both sorted and free of duplicates, and is // what the specification means by ASCII order. for (text, 0..) |c, index| { if (index > 0) try testing.expect(text[index - 1] < c); } // The same letters, no more and no fewer. for (input) |c| try testing.expect(std.mem.findScalar(u8, text, c) != null); for (text) |c| try testing.expect(std.mem.findScalar(u8, input, c) != null); // And it reads back as the same set. try testing.expect(parsed.eql(try Flags.parse(text))); try testing.expectEqual(parsed.count(), text.len); } test "fuzz flags" { for (flag_seeds) |seed| try flagsProperty(seed); try testing.fuzz({}, fuzzFlags, .{}); } fn fuzzFlags(_: void, smith: *testing.Smith) !void { var buffer: [flag_max]u8 = undefined; try flagsProperty(buffer[0..smith.slice(&buffer)]); } fn runFlags(input: []const u8) anyerror!void { var smith: testing.Smith = .{ .in = input }; var buffer: [flag_max]u8 = undefined; return flagsProperty(buffer[0..smith.slice(&buffer)]); } // -- folder names ------------------------------------------------------------- const folder_max = Dir.max_name_bytes; const folder_seeds = [_][]const u8{ "Work", "Work/Reports", "Work/Reports/Q1", "example.com", "Work//Reports", "/Work", "Work/", "", ".", "..", "Work/../Escape", "Wörk/Berichte", }; /// A path that can be encoded comes back out as the components that went in, /// and the result is a name this library recognises as a folder. fn folderProperty(input: []const u8) !void { var buffer: [folder_max]u8 = undefined; const dirname = folder.bufPath(&buffer, input, '/') catch return; // Anything this produces must be recognised by the thing that reads a // directory listing, or a folder could be created and then not listed. try testing.expect(folder.isFolder(dirname)); // The components survive the round trip. var produced = folder.components(dirname); var expected = std.mem.splitScalar(u8, input, '/'); while (expected.next()) |component| { try testing.expectEqualStrings(component, produced.next() orelse return error.MissingComponent); } try testing.expectEqual(@as(?[]const u8, null), produced.next()); // A folder name and its parent agree about their relationship, and no // path can escape the store it is in. try testing.expect(std.mem.findScalar(u8, dirname, '/') == null); if (folder.parent(dirname)) |above| { try testing.expect(folder.isFolder(above)); try testing.expect(folder.isBelow(dirname, above)); try testing.expect(!folder.isBelow(above, dirname)); } } test "fuzz folder names" { for (folder_seeds) |seed| try folderProperty(seed); try testing.fuzz({}, fuzzFolder, .{}); } fn fuzzFolder(_: void, smith: *testing.Smith) !void { var buffer: [folder_max]u8 = undefined; try folderProperty(buffer[0..smith.slice(&buffer)]); } fn runFolder(input: []const u8) anyerror!void { var smith: testing.Smith = .{ .in = input }; var buffer: [folder_max]u8 = undefined; return folderProperty(buffer[0..smith.slice(&buffer)]); } // -- the quota ledger --------------------------------------------------------- const quota_max = 4096; const quota_seeds = [_][]const u8{ "10485760S,1000C\n4211 1\n", "10485760S,1000C\n4211 1\n8320 1\n-4211 -1\n", "1000C\n", "\n", "", "nonsense\nmore nonsense\n", "1S\n9223372036854775807 1\n9223372036854775807 1\n", "1S\n-9223372036854775808 -1\n-9223372036854775808 -1\n", "100S,50X,20C\n", "10S\n 4211 1 \n", }; /// Reading a ledger never fails, and a ledger this library writes reads back /// as what was written. fn quotaProperty(input: []const u8) !void { const ledger = quota.parseLedger(input); // A sum of saturating additions cannot have overflowed, so the totals are // always usable, and clamping only ever raises them to zero. try testing.expect(ledger.usage.clamped().bytes >= 0); try testing.expect(ledger.usage.clamped().messages >= 0); // What was written is what is read: the limits and the total survive a // trip through the file format, which is what `Store.recalculateQuota` // depends on. var buffer: [128]u8 = undefined; var w: Io.Writer = .fixed(&buffer); ledger.limits.format(&w) catch return; w.writeByte('\n') catch return; ledger.usage.format(&w) catch return; w.writeByte('\n') catch return; const again = quota.parseLedger(w.buffered()); try testing.expectEqual(ledger.limits.bytes, again.limits.bytes); try testing.expectEqual(ledger.limits.messages, again.limits.messages); try testing.expectEqual(ledger.usage, again.usage); try testing.expectEqual(@as(usize, 1), again.records); try testing.expect(!again.damaged); } test "fuzz the quota ledger" { for (quota_seeds) |seed| try quotaProperty(seed); try testing.fuzz({}, fuzzQuota, .{}); } fn fuzzQuota(_: void, smith: *testing.Smith) !void { var buffer: [quota_max]u8 = undefined; try quotaProperty(buffer[0..smith.slice(&buffer)]); } fn runQuota(input: []const u8) anyerror!void { var smith: testing.Smith = .{ .in = input }; var buffer: [quota_max]u8 = undefined; return quotaProperty(buffer[0..smith.slice(&buffer)]); }