@@ 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));
+}