From 7ba88a71786392dccb03da8c37937676f46a7cd1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 14 Oct 2025 09:35:54 -0500 Subject: [PATCH] synthetic: make bytes generation more flexible --- src/synthetic/Bytes.zig | 92 ++++++++++++++++++++++++++++++------- src/synthetic/Osc.zig | 72 +++++++++++++++++------------ src/synthetic/cli/Ascii.zig | 19 +++++--- 3 files changed, 130 insertions(+), 53 deletions(-) diff --git a/src/synthetic/Bytes.zig b/src/synthetic/Bytes.zig index 40a94e0e3..7d4c34a33 100644 --- a/src/synthetic/Bytes.zig +++ b/src/synthetic/Bytes.zig @@ -1,4 +1,4 @@ -/// Generates bytes. +//! Generates bytes. const Bytes = @This(); const std = @import("std"); @@ -7,9 +7,7 @@ const Generator = @import("Generator.zig"); /// Random number generator. rand: std.Random, -/// The minimum and maximum length of the generated bytes. The maximum -/// length will be capped to the length of the buffer passed in if the -/// buffer length is smaller. +/// The minimum and maximum length of the generated bytes. min_len: usize = 1, max_len: usize = std.math.maxInt(usize), @@ -18,23 +16,79 @@ max_len: usize = std.math.maxInt(usize), /// side effect of the generator, not an intended use case. alphabet: ?[]const u8 = null, -/// Predefined alphabets. -pub const Alphabet = struct { - pub const ascii = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~"; -}; +/// Generate an alphabet given a function that returns true/false for a +/// given byte. +pub fn generateAlphabet(comptime func: fn (u8) bool) []const u8 { + @setEvalBranchQuota(3000); + var count = 0; + for (0..256) |c| { + if (func(c)) count += 1; + } + var alphabet: [count]u8 = undefined; + var i = 0; + for (0..256) |c| { + if (func(c)) { + alphabet[i] = c; + i += 1; + } + } + const result = alphabet; + return &result; +} pub fn generator(self: *Bytes) Generator { return .init(self, next); } -pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Error!void { - std.debug.assert(max_len >= 1); - const len = @min( - self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), - max_len, - ); +/// Return a copy of the Bytes, but with a new alphabet. +pub fn newAlphabet(self: *const Bytes, new_alphabet: ?[]const u8) Bytes { + return .{ + .rand = self.rand, + .alphabet = new_alphabet, + .min_len = self.min_len, + .max_len = self.max_len, + }; +} + +/// Return a copy of the Bytes, but with a new min_len. The new min +/// len cannot be more than the previous max_len. +pub fn atLeast(self: *const Bytes, new_min_len: usize) Bytes { + return .{ + .rand = self.rand, + .alphabet = self.alphabet, + .min_len = @min(self.max_len, new_min_len), + .max_len = self.max_len, + }; +} + +/// Return a copy of the Bytes, but with a new max_len. The new max_len cannot +/// be more the previous max_len. +pub fn atMost(self: *const Bytes, new_max_len: usize) Bytes { + return .{ + .rand = self.rand, + .alphabet = self.alphabet, + .min_len = @min(self.min_len, @min(self.max_len, new_max_len)), + .max_len = @min(self.max_len, new_max_len), + }; +} + +pub fn next(self: *const Bytes, writer: *std.Io.Writer, max_len: usize) std.Io.Writer.Error!void { + _ = try self.atMost(max_len).write(writer); +} + +pub fn format(self: *const Bytes, writer: *std.Io.Writer) std.Io.Writer.Error!void { + _ = try self.write(writer); +} + +/// Write some random data and return the number of bytes written. +pub fn write(self: *const Bytes, writer: *std.Io.Writer) std.Io.Writer.Error!usize { + std.debug.assert(self.min_len >= 1); + std.debug.assert(self.max_len >= self.min_len); + + const len = self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len); var buf: [8]u8 = undefined; + var remaining = len; while (remaining > 0) { const data = buf[0..@min(remaining, buf.len)]; @@ -45,6 +99,8 @@ pub fn next(self: *Bytes, writer: *std.Io.Writer, max_len: usize) Generator.Erro try writer.writeAll(data); remaining -= data.len; } + + return len; } test "bytes" { @@ -52,9 +108,11 @@ test "bytes" { var prng = std.Random.DefaultPrng.init(0); var buf: [256]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); - var v: Bytes = .{ .rand = prng.random() }; - v.min_len = buf.len; - v.max_len = buf.len; + var v: Bytes = .{ + .rand = prng.random(), + .min_len = buf.len, + .max_len = buf.len, + }; const gen = v.generator(); try gen.next(&writer, buf.len); try testing.expectEqual(buf.len, writer.buffered().len); diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig index 52940fee9..b43079e1a 100644 --- a/src/synthetic/Osc.zig +++ b/src/synthetic/Osc.zig @@ -35,19 +35,26 @@ p_valid: f64 = 1.0, p_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0), p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0), -/// The alphabet for random bytes (omitting 0x1B and 0x07). -const bytes_alphabet: []const u8 = alphabet: { - var alphabet: [256]u8 = undefined; - for (0..alphabet.len) |i| { - if (i == 0x1B or i == 0x07) { - alphabet[i] = @intCast(i + 1); - } else { - alphabet[i] = @intCast(i); - } - } - const result = alphabet; - break :alphabet &result; -}; +fn checkKvAlphabet(c: u8) bool { + return switch (c) { + std.ascii.control_code.esc, std.ascii.control_code.bel, ';', '=' => false, + else => std.ascii.isPrint(c), + }; +} + +/// The alphabet for random bytes in OSC key/value pairs (omitting 0x1B, +/// 0x07, ';', '='). +pub const kv_alphabet = Bytes.generateAlphabet(checkKvAlphabet); + +fn checkOscAlphabet(c: u8) bool { + return switch (c) { + std.ascii.control_code.esc, std.ascii.control_code.bel => false, + else => true, + }; +} + +/// The alphabet for random bytes in OSCs (omitting 0x1B and 0x07). +pub const osc_alphabet = Bytes.generateAlphabet(checkOscAlphabet); pub fn generator(self: *Osc) Generator { return .init(self, next); @@ -99,35 +106,43 @@ fn nextUnwrapped(self: *Osc, writer: *std.Io.Writer, max_len: usize) Generator.E fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKind, max_len: usize) Generator.Error!void { switch (k) { - .change_window_title => { - try writer.writeAll("0;"); // Set window title - var bytes_gen = self.bytes(); - try bytes_gen.next(writer, max_len - 2); + .change_window_title => change_window_title: { + if (max_len < 3) break :change_window_title; + try writer.print("0;{f}", .{self.bytes().atMost(max_len - 3)}); // Set window title }, - .prompt_start => { + .prompt_start => prompt_start: { + if (max_len < 4) break :prompt_start; + var remaining = max_len; + try writer.writeAll("133;A"); // Start prompt + remaining -= 4; // aid - if (self.rand.boolean()) { - var bytes_gen = self.bytes(); - bytes_gen.max_len = 16; + if (self.rand.boolean()) aid: { + if (remaining < 6) break :aid; try writer.writeAll(";aid="); - try bytes_gen.next(writer, max_len); + remaining -= 5; + remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(16, remaining)).write(writer); } // redraw - if (self.rand.boolean()) { + if (self.rand.boolean()) redraw: { + if (remaining < 9) break :redraw; try writer.writeAll(";redraw="); if (self.rand.boolean()) { try writer.writeAll("1"); } else { try writer.writeAll("0"); } + remaining -= 9; } }, - .prompt_end => try writer.writeAll("133;B"), // End prompt + .prompt_end => prompt_end: { + if (max_len < 4) break :prompt_end; + try writer.writeAll("133;B"); // End prompt + }, } } @@ -139,14 +154,11 @@ fn nextUnwrappedInvalidExact( ) Generator.Error!void { switch (k) { .random => { - var bytes_gen = self.bytes(); - try bytes_gen.next(writer, max_len); + try self.bytes().atMost(max_len).format(writer); }, .good_prefix => { - try writer.writeAll("133;"); - var bytes_gen = self.bytes(); - try bytes_gen.next(writer, max_len - 4); + try writer.print("133;{f}", .{self.bytes().atMost(max_len - 4)}); }, } } @@ -154,7 +166,7 @@ fn nextUnwrappedInvalidExact( fn bytes(self: *const Osc) Bytes { return .{ .rand = self.rand, - .alphabet = bytes_alphabet, + .alphabet = osc_alphabet, }; } diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index b2d57fa88..22ca1ffb5 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -3,12 +3,21 @@ const Ascii = @This(); const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const synthetic = @import("../main.zig"); +const Bytes = @import("../Bytes.zig"); const log = std.log.scoped(.@"terminal-stream-bench"); pub const Options = struct {}; +fn checkAsciiAlphabet(c: u8) bool { + return switch (c) { + ' ' => false, + else => std.ascii.isPrint(c), + }; +} + +pub const ascii = Bytes.generateAlphabet(checkAsciiAlphabet); + /// Create a new terminal stream handler for the given arguments. pub fn create( alloc: Allocator, @@ -23,12 +32,10 @@ pub fn destroy(self: *Ascii, alloc: Allocator) void { alloc.destroy(self); } -pub fn run(self: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { - _ = self; - - var gen: synthetic.Bytes = .{ +pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void { + var gen: Bytes = .{ .rand = rand, - .alphabet = synthetic.Bytes.Alphabet.ascii, + .alphabet = ascii, }; while (true) {