~caolan/zig-netstring

94803d2d107c01c10c0fd78ef7edd8ba61e3ced7 — Caolan McMahon 2 years ago
first commit
4 files changed, 194 insertions(+), 0 deletions(-)

A .gitignore
A README.md
A build.zig
A src/netstring.zig
A  => .gitignore +2 -0
@@ 1,2 @@
zig-cache
zig-out

A  => README.md +37 -0
@@ 1,37 @@
# zig-netstring

[Zig](ziglang.org) parser for [netstrings](http://cr.yp.to/proto/netstrings.txt).

## Usage

Add `src/netstring.zig` as a package in your `build.zig`:

```zig
example.addPackagePath("netstring", "path/to/deps/netstring/src/netstring.zig");
```

## Example

```zig
const netstring = @import("netstring");
const std = @import("std");

pub fn main() !void {
    var input = std.io.fixedBufferStream("5:hello,6:world!,");
    var parser = netstring.netStringParser(input.reader());

    // Repeat while the parser can find another netstring
    while (try parser.next()) {
        // Read the current netstring and print it
        const reader = parser.reader();
        const msg = try reader.readAllAlloc(std.heap.page_allocator, 4096);
        std.debug.print("{s}\n", .{msg});
    }
}
```

## Running test suite

```
$ zig build tests
```

A  => build.zig +13 -0
@@ 1,13 @@
const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    var tests = b.addTest("src/netstring.zig");
    tests.setBuildMode(mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&tests.step);
}

A  => src/netstring.zig +142 -0
@@ 1,142 @@
const std = @import("std");
const testing = std.testing;

const MAX_DIGITS = std.fmt.count("{d}", .{std.math.maxInt(usize)});

pub fn NetStringParser(comptime ReaderType: type) type {
    const CountingReader = std.io.CountingReader(ReaderType);

    return struct {
        child_reader: CountingReader,
        buffer: [MAX_DIGITS]u8 = undefined,
        end: usize = 0,

        const Error = ReaderType.Error;
        const Reader = std.io.Reader(*@This(), Error, read);

        /// Aligns reader at beginning of next netstring. Returns true if
        /// another netstring was found, false if not.
        pub fn next(self: *@This()) !bool {
            var r = self.child_reader.reader();
            // move stream to end of current netstring
            if (self.child_reader.bytes_read < self.end) {
                try r.skipBytes(self.end - self.child_reader.bytes_read, .{});
            }
            // read size of netstring
            const size_str = try r.readUntilDelimiterOrEof(&self.buffer, ':');
            if (size_str) |s| {
                const size = try std.fmt.parseUnsigned(usize, s, 10);
                self.end = self.child_reader.bytes_read + size + 1;
                return true;
            } else {
                return false;
            }
        }

        /// Reads bytes from the current netstring returning number of bytes read.
        /// If the number of bytes read is 0 it means the stream reached the end
        /// of the current netstring.
        pub fn read(self: *@This(), buf: []u8) Error!usize {
            // read until one byte before end because of tailing comma
            if (self.child_reader.bytes_read + 1 >= self.end) {
                return 0;
            }
            var r = self.child_reader.reader();
            const bytes_read = self.child_reader.bytes_read;
            const max = std.math.min(buf.len, self.end - 1 - bytes_read);
            const n = try r.read(buf[0..max]);
            return n;
        }

        /// Returns a std.io.Reader for the current netstring. Calling `next()`
        /// on this parser will move the reader onto the next netstring. This
        /// must be done before the first netstring can be read.
        pub fn reader(self: *@This()) Reader {
            return .{ .context = self };
        }
    };
}

pub fn netStringParser(reader: anytype) NetStringParser(@TypeOf(reader)) {
    return .{ .child_reader = std.io.countingReader(reader) };
}

test "parse empty input stream" {
    var input = std.io.fixedBufferStream("");
    var p = netStringParser(input.reader());
    var msg: [1024]u8 = undefined;
    try testing.expectEqual(@intCast(usize, 0), try p.reader().read(&msg));
    try testing.expectEqual(false, try p.next());
}

test "parse single netstring" {
    var input = std.io.fixedBufferStream("5:hello,");
    var p = netStringParser(input.reader());
    var msg: [1024]u8 = undefined;
    const r = p.reader();
    try testing.expectEqual(true, try p.next());
    const bytes = try r.read(&msg);
    try testing.expectEqual(@intCast(usize, 5), bytes);
    try testing.expectEqualSlices(u8, "hello", msg[0..bytes]);
    try testing.expectEqual(@intCast(usize, 0), try r.read(&msg));
    try testing.expectEqual(false, try p.next());
}

test "parse multiple netstrings" {
    var input = std.io.fixedBufferStream("5:hello,1:,,7: world!,");
    var p = netStringParser(input.reader());
    var msg: [1024]u8 = undefined;
    const r = p.reader();
    try testing.expectEqual(true, try p.next());
    {
        const bytes = try r.read(&msg);
        try testing.expectEqual(@intCast(usize, 5), bytes);
        try testing.expectEqualSlices(u8, "hello", msg[0..bytes]);
        try testing.expectEqual(@intCast(usize, 0), try r.read(&msg));
    }
    try testing.expectEqual(true, try p.next());
    {
        const bytes = try r.read(&msg);
        try testing.expectEqual(@intCast(usize, 1), bytes);
        try testing.expectEqualSlices(u8, ",", msg[0..bytes]);
        try testing.expectEqual(@intCast(usize, 0), try r.read(&msg));
    }
    try testing.expectEqual(true, try p.next());
    {
        const bytes = try r.read(&msg);
        try testing.expectEqual(@intCast(usize, 7), bytes);
        try testing.expectEqualSlices(u8, " world!", msg[0..bytes]);
        try testing.expectEqual(@intCast(usize, 0), try r.read(&msg));
    }
    try testing.expectEqual(false, try p.next());
}

test "calling next before completely reading current netstring" {
    var input = std.io.fixedBufferStream("3:foo,3:bar,");
    var p = netStringParser(input.reader());
    var msg: [1]u8 = undefined;
    const r = p.reader();
    try testing.expectEqual(true, try p.next());
    {
        const bytes = try r.read(&msg);
        try testing.expectEqual(@intCast(usize, 1), bytes);
        try testing.expectEqualSlices(u8, "f", msg[0..bytes]);
    }
    try testing.expectEqual(true, try p.next());
    {
        const bytes = try r.read(&msg);
        try testing.expectEqual(@intCast(usize, 1), bytes);
        try testing.expectEqualSlices(u8, "b", msg[0..bytes]);
    }
    try testing.expectEqual(false, try p.next());
    try testing.expectEqual(@intCast(usize, 0), try r.read(&msg));
}

test "invalid input" {
    var input = std.io.fixedBufferStream("hello");
    var p = netStringParser(input.reader());
    var msg: [1024]u8 = undefined;
    const r = p.reader();
    try testing.expectError(error.InvalidCharacter, p.next());
    try testing.expectEqual(@intCast(usize, 0), try r.read(&msg));
}