mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
This updates our synthetic generator for UTF-8 to expose:
- Flags to change 1/2/3/4-byte UTF-8 character distribution
- Flags to have only printable characters so we can benchmark
pure UTF-8 vs our control sequence finder.
- Flags to have invalid characters so we can benchmark our error
handling.
This also adds an AGENTS.md to src/benchmark so agents can do the right
thing more easily.
230 lines
7.1 KiB
Zig
230 lines
7.1 KiB
Zig
/// Generates UTF-8.
|
|
///
|
|
/// This doesn't yet generate multi-codepoint graphemes, but it
|
|
/// has the ability to generate a custom distribution of UTF-8
|
|
/// encoding lengths (1, 2, 3, or 4 bytes).
|
|
const Utf8 = @This();
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Generator = @import("Generator.zig");
|
|
|
|
/// Possible UTF-8 encoding lengths.
|
|
pub const Utf8Len = enum(u3) {
|
|
one = 1,
|
|
two = 2,
|
|
three = 3,
|
|
four = 4,
|
|
};
|
|
|
|
const InvalidSequence = struct {
|
|
len: u3,
|
|
bytes: [4]u8,
|
|
|
|
fn slice(self: *const InvalidSequence) []const u8 {
|
|
return self.bytes[0..self.len];
|
|
}
|
|
};
|
|
|
|
const invalid_sequences = [_]InvalidSequence{
|
|
.{ .len = 1, .bytes = .{ 0x80, 0x00, 0x00, 0x00 } },
|
|
.{ .len = 1, .bytes = .{ 0xC0, 0x00, 0x00, 0x00 } },
|
|
.{ .len = 1, .bytes = .{ 0xFF, 0x00, 0x00, 0x00 } },
|
|
.{ .len = 2, .bytes = .{ 0xC2, 0x20, 0x00, 0x00 } },
|
|
.{ .len = 2, .bytes = .{ 0xC0, 0xAF, 0x00, 0x00 } },
|
|
.{ .len = 2, .bytes = .{ 0x80, 0x80, 0x00, 0x00 } },
|
|
.{ .len = 3, .bytes = .{ 0xED, 0xA0, 0x80, 0x00 } },
|
|
.{ .len = 3, .bytes = .{ 0xE2, 0x28, 0x7A, 0x00 } },
|
|
.{ .len = 4, .bytes = .{ 0xF0, 0x90, 0x28, 0x7A } },
|
|
};
|
|
|
|
/// 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.
|
|
min_len: usize = 1,
|
|
max_len: usize = std.math.maxInt(usize),
|
|
|
|
/// Probability of a specific UTF-8 encoding length being generated.
|
|
/// The probabilities are weighted relative to each other, so they
|
|
/// can sum greater than 1.0. A length of weight 1.0 and a length
|
|
/// of weight 2.0 will have a 2:1 chance of the latter being
|
|
/// selected.
|
|
///
|
|
/// If a UTF-8 encoding of a chosen length can't fit into the remaining
|
|
/// buffer, a smaller length will be chosen. For small buffers this may
|
|
/// skew the distribution of lengths.
|
|
p_length: std.enums.EnumArray(Utf8Len, f64) = .initFill(1.0),
|
|
|
|
/// If true, ASCII codepoints are limited to printable ASCII.
|
|
ascii_printable_only: bool = false,
|
|
|
|
/// Probability that the next generated sequence is malformed UTF-8.
|
|
/// This is checked for each emitted sequence while filling a buffer.
|
|
invalid_rate: f64 = 0,
|
|
|
|
pub fn generator(self: *Utf8) Generator {
|
|
return .init(self, next);
|
|
}
|
|
|
|
pub fn next(self: *Utf8, writer: *std.Io.Writer, max_len: usize) Generator.Error!void {
|
|
const len = @min(
|
|
self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len),
|
|
max_len,
|
|
);
|
|
|
|
var rem: usize = len;
|
|
while (rem > 0) {
|
|
if (try self.writeInvalid(writer, rem)) |written| {
|
|
rem -= written;
|
|
continue;
|
|
}
|
|
|
|
const written = try self.writeValid(writer, rem);
|
|
rem -= written;
|
|
}
|
|
}
|
|
|
|
fn writeInvalid(
|
|
self: *Utf8,
|
|
writer: *std.Io.Writer,
|
|
rem: usize,
|
|
) Generator.Error!?usize {
|
|
if (self.invalid_rate <= 0 or self.rand.float(f64) >= self.invalid_rate) {
|
|
return null;
|
|
}
|
|
|
|
const seq = self.invalidSequence(rem) orelse return null;
|
|
try writer.writeAll(seq.slice());
|
|
return seq.len;
|
|
}
|
|
|
|
fn writeValid(
|
|
self: *Utf8,
|
|
writer: *std.Io.Writer,
|
|
rem: usize,
|
|
) Generator.Error!usize {
|
|
while (true) {
|
|
const utf8_len = self.utf8Len(rem);
|
|
|
|
// Generate a UTF-8 sequence that encodes to this length.
|
|
const cp: u21 = switch (utf8_len) {
|
|
.one => if (self.ascii_printable_only)
|
|
self.rand.intRangeAtMostBiased(u21, 0x20, 0x7E)
|
|
else
|
|
self.rand.intRangeAtMostBiased(u21, 0x00, 0x7F),
|
|
.two => self.rand.intRangeAtMostBiased(u21, 0x80, 0x7FF),
|
|
.three => self.rand.intRangeAtMostBiased(u21, 0x800, 0xFFFF),
|
|
.four => self.rand.intRangeAtMostBiased(u21, 0x10000, 0x10FFFF),
|
|
};
|
|
|
|
assert(std.unicode.utf8CodepointSequenceLength(
|
|
cp,
|
|
) catch unreachable == @intFromEnum(utf8_len));
|
|
|
|
var buf: [4]u8 = undefined;
|
|
const l = std.unicode.utf8Encode(
|
|
cp,
|
|
&buf,
|
|
) catch |err| switch (err) {
|
|
// Impossible because our generation above is hardcoded to
|
|
// produce a valid range. If not, a bug.
|
|
error.CodepointTooLarge => unreachable,
|
|
|
|
// Possible, in which case we redo the loop and encode nothing.
|
|
error.Utf8CannotEncodeSurrogateHalf => continue,
|
|
};
|
|
|
|
try writer.writeAll(buf[0..l]);
|
|
return l;
|
|
}
|
|
}
|
|
|
|
fn utf8Len(self: *Utf8, rem: usize) Utf8Len {
|
|
const Indexer = @TypeOf(self.p_length).Indexer;
|
|
const idx = self.rand.weightedIndex(f64, &self.p_length.values);
|
|
var utf8_len = Indexer.keyForIndex(idx);
|
|
assert(rem > 0);
|
|
while (@intFromEnum(utf8_len) > rem) {
|
|
// If the chosen length can't fit into the remaining buffer,
|
|
// choose a smaller length.
|
|
utf8_len = @enumFromInt(@intFromEnum(utf8_len) - 1);
|
|
}
|
|
return utf8_len;
|
|
}
|
|
|
|
fn invalidSequence(self: *Utf8, rem: usize) ?InvalidSequence {
|
|
const candidates = &invalid_sequences;
|
|
|
|
var valid_idx: [candidates.len]usize = undefined;
|
|
var valid_len: usize = 0;
|
|
for (candidates, 0..) |candidate, i| {
|
|
if (candidate.len > rem) continue;
|
|
valid_idx[valid_len] = i;
|
|
valid_len += 1;
|
|
}
|
|
|
|
if (valid_len == 0) return null;
|
|
return candidates[valid_idx[self.rand.uintLessThan(usize, valid_len)]];
|
|
}
|
|
|
|
test "utf8" {
|
|
const testing = std.testing;
|
|
var prng = std.Random.DefaultPrng.init(0);
|
|
var buf: [256]u8 = undefined;
|
|
var writer: std.Io.Writer = .fixed(&buf);
|
|
var v: Utf8 = .{ .rand = prng.random() };
|
|
v.min_len = buf.len;
|
|
v.max_len = buf.len;
|
|
const gen = v.generator();
|
|
try gen.next(&writer, buf.len);
|
|
const result = writer.buffered();
|
|
try testing.expectEqual(256, result.len);
|
|
try testing.expect(std.unicode.utf8ValidateSlice(result));
|
|
}
|
|
|
|
test "utf8 printable ascii only" {
|
|
const testing = std.testing;
|
|
var prng = std.Random.DefaultPrng.init(0);
|
|
var buf: [256]u8 = undefined;
|
|
var writer: std.Io.Writer = .fixed(&buf);
|
|
var v: Utf8 = .{
|
|
.rand = prng.random(),
|
|
.ascii_printable_only = true,
|
|
};
|
|
v.min_len = buf.len;
|
|
v.max_len = buf.len;
|
|
v.p_length.set(.one, 1.0);
|
|
v.p_length.set(.two, 0.0);
|
|
v.p_length.set(.three, 0.0);
|
|
v.p_length.set(.four, 0.0);
|
|
|
|
const gen = v.generator();
|
|
try gen.next(&writer, buf.len);
|
|
const result = writer.buffered();
|
|
try testing.expectEqual(256, result.len);
|
|
try testing.expect(std.unicode.utf8ValidateSlice(result));
|
|
for (result) |c| try testing.expect(std.ascii.isPrint(c));
|
|
}
|
|
|
|
test "utf8 malformed output" {
|
|
const testing = std.testing;
|
|
var prng = std.Random.DefaultPrng.init(0);
|
|
var buf: [256]u8 = undefined;
|
|
var writer: std.Io.Writer = .fixed(&buf);
|
|
var v: Utf8 = .{
|
|
.rand = prng.random(),
|
|
.invalid_rate = 1.0,
|
|
};
|
|
v.min_len = buf.len;
|
|
v.max_len = buf.len;
|
|
|
|
const gen = v.generator();
|
|
try gen.next(&writer, buf.len);
|
|
const result = writer.buffered();
|
|
try testing.expectEqual(256, result.len);
|
|
try testing.expect(!std.unicode.utf8ValidateSlice(result));
|
|
}
|