bytes

tape
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

FunctionDoes
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.

tape
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 bytes

copy, 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

FunctionReturns
eq(a, b) -> booltrue when a and b have the same bytes
eq_ignore_ascii_case(a, b) -> boolthe 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) -> booltrue when s begins with prefix
ends_with(s, suffix) -> booltrue 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 "".

tape
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");        // true

Searching

FunctionReturns
index_of(haystack, needle) -> ?i64offset of the first needle, or null
last_index_of(haystack, needle) -> ?i64offset of the last needle, or null
index_of_byte(s, b: u8) -> ?i64offset of the first byte equal to b, or null
contains(haystack, needle) -> booltrue when needle occurs
count(haystack, needle) -> i64number 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.

tape
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

FunctionReturns
trim(s) -> []const u8s without leading and trailing whitespace
trim_left(s) -> []const u8s without leading whitespace
trim_right(s) -> []const u8s without trailing whitespace
trim_byte(s, b: u8) -> []const u8s 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.

tape
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.

FunctionYields
split(s, sep) -> SplitIterthe parts of s between occurrences of sep
split_byte(s, sep: u8) -> SplitByteIterthe same with a one-byte separator
lines(s) -> LineIterthe 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.

tape
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.

tape
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):

tape
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();      // null

The 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

tape
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:

tape
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

tape
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.

tape
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", "=");   // null

Last modified: