/// 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)); }