My code in working through the CS Primer material.
// https://csprimer.com/watch/varint/

const std = @import("std");

test "VarintByte has correct size & alignment" {
    try comptime std.testing.expectEqual(1, @sizeOf(VarintByte));
    try comptime std.testing.expectEqual(1, @alignOf(VarintByte));
}

test "VarintByte bits are ordered correctly" {
    try comptime std.testing.expectEqual(0x80, @bitCast(u8, VarintByte{ .has_more = true, .value = 0 }));
}

pub const VarintByte = packed struct {
    value: u7,
    has_more: bool,
};

// Encoding

test "encode literals" {
    var buffer: [MAX_BYTES]VarintByte = undefined;
    inline for (.{
        .{ 0, [_]u8{0x00} },
        .{ 1, [_]u8{0x01} },
        .{ 150, [_]u8{ 0x96, 0x01 } },
        .{ std.math.maxInt(u64), [_]u8{0xFF} ** (MAX_BYTES - 1) ++ [_]u8{0x01} },
    }) |vals| {
        const input = vals[0];
        const expt_result = vals[1];

        const result = encode(input, &buffer);
        try std.testing.expectEqualSlices(u8, &expt_result, @ptrCast([]u8, result));
    }
}

pub fn encode(value: u64, buffer: *[MAX_BYTES]VarintByte) []VarintByte {
    var val = value;
    for (buffer, 0..) |*item, i| {
        item.* = VarintByte{ .has_more = true, .value = @truncate(u7, val) };

        val = val >> 7;
        if (val == 0) {
            item.*.has_more = false;
            return buffer[0 .. i + 1];
        }
    }
    unreachable;
}

// Decoding

// TODO compiler bug!
// test "decode literals" {
//     inline for (.{
//         .{ @as(u64, 0), [_]u8{0x00} },
//         .{ @as(u64, 1), [_]u8{0x01} },
//         .{ @as(u64, 150), [_]u8{ 0x96, 0x01 } },
//         .{ @as(u64, std.math.maxInt(u64)), [_]u8{0xFF} ** (MAX_BYTES - 1) ++ [_]u8{0x01} },
//     }) |vals| {
//         const expt_result = vals[0];
//         const input: []const u8 = &vals[1];

//         const result = try decode(@ptrCast([]const VarintByte, input));
//         try std.testing.expectEqual(expt_result, result);
//     }
// }
test "decode literal 0" {
    const expt_result = @as(u64, 0);
    const input: []const u8 = &[_]u8{0x00};

    const result = try decode(@ptrCast([]const VarintByte, input));
    try std.testing.expectEqual(expt_result, result);
}
test "decode literal 1" {
    const expt_result = @as(u64, 1);
    const input: []const u8 = &[_]u8{0x01};

    const result = try decode(@ptrCast([]const VarintByte, input));
    try std.testing.expectEqual(expt_result, result);
}
test "decode literal 150" {
    const expt_result = @as(u64, 150);
    const input: []const u8 = &[_]u8{ 0x96, 0x01 };

    const result = try decode(@ptrCast([]const VarintByte, input));
    try std.testing.expectEqual(expt_result, result);
}
test "decode literal max value" {
    const expt_result = @as(u64, std.math.maxInt(u64));
    const input: []const u8 = &([_]u8{0xFF} ** (MAX_BYTES - 1) ++ [_]u8{0x01});

    const result = try decode(@ptrCast([]const VarintByte, input));
    try std.testing.expectEqual(expt_result, result);
}

const DecodeError = error{NoTermination};

pub fn decode(buffer: []const VarintByte) DecodeError!u64 {
    var terminal_pos: usize = inline for (0..MAX_BYTES + 1) |j| {
        if (j >= MAX_BYTES or j >= buffer.len) {
            return DecodeError.NoTermination;
        }
        const item = buffer[j];
        if (!item.has_more and (j < MAX_BYTES - 1 or item.value <= 1)) {
            break j;
        }
    } else unreachable;

    var result: u64 = 0;
    var bytes_rev = std.mem.reverseIterator(buffer[0 .. terminal_pos + 1]);
    while (bytes_rev.next()) |item| {
        result = (result << 7) | item.value;
    }
    return result;
}

/// The maximum number of bytes required to store a 64-bit varint.
const MAX_BYTES = 10;