bytes
import bytes from "bytes";bytes works on byte views: []const u8 for input (a string or a literal
is accepted as one, see Strings as byte views) and
[]u8 for buffers it writes into. It compares, searches, trims and splits
text, and copies and fills buffers. It is a pure t0 module: it never
allocates, has no state, and works in every profile, including kernel code.
Results that are text are sub-views of the input, not copies. They see the same memory, so they must not outlive what they came from.
The functions work on bytes. “Whitespace” and “ignore case” mean ASCII only (the ascii classes); bytes from 128 up only ever match themselves, so UTF-8 text is safe to search, trim and split.
Missing results are null (?i64, ?[]const u8), never -1. A few
functions stop the program with a trap when
the caller breaks their contract; each one says so below.
Writing into buffers
| Function | Does |
|---|---|
copy(dst: []u8, src: []const u8) | Copies src into the start of dst |
move(dst: []u8, src: []const u8) | The same, correct when dst and src overlap |
fill(dst: []u8, val: u8) | Sets every byte of dst to val |
zero(dst: []u8) | Sets every byte of dst to 0 |
copy and move write src.len bytes and leave the rest of dst alone.
They trap with bytes.copy: destination too short (or bytes.move: ...)
when dst is shorter than src. Use move when the two views can be parts
of the same buffer; with copy the result is then unspecified.
var buf: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0];
bytes.copy(buf[0..8], "tape"); // buf: "tape" then four 0 bytes
bytes.fill(buf[4..8], 33); // buf: "tape!!!!"
bytes.move(buf[1..8], buf[0..7]); // shift right by one: "ttape!!!"
bytes.zero(buf[5..8]); // "ttape" then three 0 bytescopy, move, fill and zero are
@constant_time: they
branch only on lengths and addresses, so constant-time code can copy and
wipe keys with them. Comparing and searching stop at the first difference
and are not.
Comparing
| Function | Returns |
|---|---|
eq(a, b) -> bool | true when a and b have the same bytes |
eq_ignore_ascii_case(a, b) -> bool | the same, with A-Z equal to a-z |
compare(a, b) -> i64 | -1, 0 or 1: a sorts before, equal to, or after b |
starts_with(s, prefix) -> bool | true when s begins with prefix |
ends_with(s, suffix) -> bool | true when s ends with suffix |
All parameters are []const u8. compare orders byte by byte as unsigned
values, and a view that is a prefix of the other sorts first. Every view
starts and ends with "".
let same: bool = bytes.eq("tape", "tape"); // true
let ci: bool = bytes.eq_ignore_ascii_case("Content-Type", "content-type"); // true
let order: i64 = bytes.compare("apple", "apricot"); // -1
let shorter: i64 = bytes.compare("ab", "abc"); // -1
let is_cmd: bool = bytes.starts_with("--help", "--"); // true
let is_tape: bool = bytes.ends_with("main.tape", ".tape"); // trueSearching
| Function | Returns |
|---|---|
index_of(haystack, needle) -> ?i64 | offset of the first needle, or null |
last_index_of(haystack, needle) -> ?i64 | offset of the last needle, or null |
index_of_byte(s, b: u8) -> ?i64 | offset of the first byte equal to b, or null |
contains(haystack, needle) -> bool | true when needle occurs |
count(haystack, needle) -> i64 | number of non-overlapping occurrences |
An empty needle is found at 0 by index_of and at the end by
last_index_of. count counts from left to right without overlap
("aaaa" contains "aa" twice) and traps with bytes.count: empty needle
when needle is empty.
let line: []const u8 = "key=value";
let at: ?i64 = bytes.index_of(line, "="); // 3
let ext: ?i64 = bytes.last_index_of("a.tar.gz", "."); // 5
let nl: ?i64 = bytes.index_of_byte("one\ntwo", 10); // 3
let none: ?i64 = bytes.index_of("tape", "x"); // null
let has: bool = bytes.contains("hello world", "o w"); // true
let commas: i64 = bytes.count("a,b,c", ","); // 2
if (at != null) {
let key: []const u8 = line[0..at!]; // "key"
}Trimming
| Function | Returns |
|---|---|
trim(s) -> []const u8 | s without leading and trailing whitespace |
trim_left(s) -> []const u8 | s without leading whitespace |
trim_right(s) -> []const u8 | s without trailing whitespace |
trim_byte(s, b: u8) -> []const u8 | s without leading and trailing bytes equal to b |
Whitespace is space, tab, newline, vertical tab, form feed and carriage
return. The result is a sub-view of s.
let t: []const u8 = bytes.trim(" hello \r\n"); // "hello"
let l: []const u8 = bytes.trim_left("\tindented"); // "indented"
let r: []const u8 = bytes.trim_right("line\r\n"); // "line"
let q: []const u8 = bytes.trim_byte("\"quoted\"", 34); // "quoted"Splitting
split, split_byte and lines return iterators, so you use them with
for. Each part is a sub-view of the input.
| Function | Yields |
|---|---|
split(s, sep) -> SplitIter | the parts of s between occurrences of sep |
split_byte(s, sep: u8) -> SplitByteIter | the same with a one-byte separator |
lines(s) -> LineIter | the lines of s, without \n or \r\n |
split yields one more part than there are separators: a separator at the
start or end, or two in a row, give empty parts, and an empty s gives one
empty part. It traps with bytes.split: empty separator when sep is empty.
var total: i64 = 0;
for (field in bytes.split("3,,4", ",")) {
// "3", "", "4"
total = total + field.len;
}
for (word in bytes.split_byte("a b c", 32)) {
// "a", "b", "c"
}lines ends a line at \n and also removes a \r just before it. A line
ending at the very end doesn’t start an extra empty line, and an empty view
has no lines.
var count: i64 = 0;
for (line in bytes.lines("first\r\nsecond\n\nfourth\n")) {
// "first", "second", "", "fourth"
count = count + 1;
}You can also call next() yourself. It returns the next part, and null
after the last one (and on every later call):
var it: bytes.SplitIter = bytes.split("x=1", "=");
let name: ?[]const u8 = it.next(); // "x"
let value: ?[]const u8 = it.next(); // "1"
let end: ?[]const u8 = it.next(); // nullThe iterator types (SplitIter, SplitByteIter, LineIter) hold views of
the input. Make them with split, split_byte and lines rather than by
setting their fields.
Elements as bytes
let verts: [6]f32 = [0.0, 0.5, -0.5, -0.5, 0.5, -0.5];
gl.glBufferData(gl.GL_ARRAY_BUFFER, bytes.of_f32(verts[0..6]), gl.GL_STATIC_DRAW);bytes.of_f32(xs) views the numbers as their bytes (xs.len * 4 of
them), in memory order, without copying: what a GPU buffer, a file or a
socket takes. There is one for each number type: of_f64, of_i16,
of_u16, of_i32, of_u32, of_i64, of_u64.
For your own struct, the generator makes one:
pub struct Vertex { x: f32; y: f32; u: f32; v: f32; }
@tape { bytes.gen_of(Vertex); } // of_Vertex(xs: []const Vertex) -> []const u8
gl.glBufferData(gl.GL_ARRAY_BUFFER, of_Vertex(quad[0..4]), gl.GL_STATIC_DRAW);Cutting
pub struct Cut { before: []const u8; after: []const u8; }
pub fn cut(s: []const u8, sep: []const u8) -> ?Cut;cut splits s around the first sep: before is everything before it,
after everything after it. When sep doesn’t occur the result is null.
Both sides are sub-views of s.
let kv: ?bytes.Cut = bytes.cut("name = tape", "=");
if (kv != null) {
let key: []const u8 = bytes.trim(kv!.before); // "name"
let value: []const u8 = bytes.trim(kv!.after); // "tape"
}
let missing: ?bytes.Cut = bytes.cut("no separator", "="); // nullLast modified: