diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | build.zig | 35 | ||||
-rw-r--r-- | sample.torrent | 1 | ||||
-rw-r--r-- | src/bencode.zig | 239 | ||||
-rw-r--r-- | src/main.zig | 7 |
6 files changed, 291 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e73c965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache/ +zig-out/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0f1eeb --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# zbt + +Bittorrent client using Zig and Capy for UI. + +Somewhat inspired by codecrafters bittorrent course. + +Built as a learning exercise.
\ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..96ba17c --- /dev/null +++ b/build.zig @@ -0,0 +1,35 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "zbt", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(exe); + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + //b.installArtifact(unit_tests); // Useful if you want to debug the test binary with lldb or something + + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/sample.torrent b/sample.torrent new file mode 100644 index 0000000..44d1411 --- /dev/null +++ b/sample.torrent @@ -0,0 +1 @@ +d8:announce55:http://bittorrent-test-tracker.codecrafters.io/announce10:created by13:mktorrent 1.14:infod6:lengthi92063e4:name10:sample.txt12:piece lengthi32768e6:pieces60:vz*kg&-n"uvfVsnR5
z r'ee
\ No newline at end of file diff --git a/src/bencode.zig b/src/bencode.zig new file mode 100644 index 0000000..b78ee9f --- /dev/null +++ b/src/bencode.zig @@ -0,0 +1,239 @@ +//! Bencoding +//! See specification here https://wiki.theory.org/BitTorrentSpecification#Bencoding + +const std = @import("std"); + +pub const Error = error.Malformatted || std.io.AnyReader.Error; + +// All content is owned by the BValue and must be freed with deinit. +pub const BValue = union(enum) { + string: []const u8, + int: i64, + list: std.ArrayList(BValue), + dict: std.StringArrayHashMap(BValue), + + pub fn deinit(self: *BValue, a: std.mem.Allocator) void { + switch (self.*) { + .string => |s| { + a.free(s); + }, + .list => |*l| { + for (l.items) |*i| { + i.deinit(a); + } + l.deinit(); + }, + .dict => |*d| { + var it = d.iterator(); + while (it.next()) |entry| { + a.free(entry.key_ptr.*); + entry.value_ptr.*.deinit(a); + } + d.deinit(); + }, + .int => {}, + } + } +}; + +pub fn bdecodeBuf(a: std.mem.Allocator, buf: []const u8) !BValue { + var fbs = std.io.fixedBufferStream(buf); + return try bdecode(a, fbs.reader()); +} + +pub fn bdecode(a: std.mem.Allocator, base_reader: anytype) anyerror!BValue { + var reader = PeekStream.init(base_reader.any()); + return bdecodeInner(a, &reader, 0); +} + +const PeekStream = std.io.PeekStream(.{ .Static = 1 }, std.io.AnyReader); + +// Note: uses defined types only to avoid trying to recursively evaulate this function +// at compile time, otherwise we run into https://github.com/ziglang/zig/issues/13724 +fn bdecodeInner(a: std.mem.Allocator, peekStream: *PeekStream, depth: u32) !BValue { + if (depth > 100) { + // TODO diagnostic... + return error.Malformatted; + } + var reader = peekStream.reader(); + var byte = try reader.readByte(); + if (std.ascii.isDigit(byte)) { + try peekStream.putBackByte(byte); + return .{ .string = try readString(a, peekStream) }; + } else { + switch (byte) { + 'i' => { + const max_len = comptime std.fmt.comptimePrint("{}", .{std.math.minInt(i64)}).len; + var s = reader.readUntilDelimiterAlloc(a, 'e', max_len) catch return error.Malformatted; + defer a.free(s); + const i = std.fmt.parseInt(i64, s, 10) catch return error.Malformatted; + return .{ .int = i }; + }, + 'l' => { + var list = std.ArrayList(BValue).init(a); + errdefer { + for (list.items) |*i| { + i.deinit(a); + } + list.deinit(); + } + while (true) { + const b2 = try reader.readByte(); + if (b2 == 'e') break; + try peekStream.putBackByte(b2); + var val = try bdecodeInner(a, peekStream, depth + 1); + errdefer val.deinit(a); + try list.append(val); + } + return .{ .list = list }; + }, + 'd' => { + var dict = std.StringArrayHashMap(BValue).init(a); + errdefer { + var it = dict.iterator(); + while (it.next()) |entry| { + a.free(entry.key_ptr.*); + entry.value_ptr.*.deinit(a); + } + dict.deinit(); + } + lp: while (true) { + const b2 = try reader.readByte(); + if (b2 == 'e') break :lp; + try peekStream.putBackByte(b2); + var key = try readString(a, peekStream); + errdefer a.free(key); + var val = try bdecode(a, reader); + errdefer val.deinit(a); + try dict.put(key, val); + } + return .{ .dict = dict }; + }, + else => return error.Malformatted, // TODO diagnostics + } + } +} + +// Result is owned by the caller and must be freed +fn readString(a: std.mem.Allocator, peekStream: *PeekStream) ![]const u8 { + var reader = peekStream.reader(); + const max_len = comptime std.fmt.comptimePrint("{}", .{std.math.maxInt(usize)}).len; + const str_len_s = reader.readUntilDelimiterAlloc(a, ':', max_len) catch { + return error.Malformatted; + }; + defer a.free(str_len_s); + var strlen = std.fmt.parseInt(usize, str_len_s, 10) catch return error.Malformatted; + var string = try a.alloc(u8, strlen); + errdefer a.free(string); + reader.readNoEof(string) catch return error.Malformatted; + return string; +} + +test "bdecode empty" { + var a = std.testing.allocator; + try std.testing.expectError(error.EndOfStream, bdecodeBuf(a, "")); +} + +test "bdecode too short" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "1")); +} + +test "bdecode plain number" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "12")); +} + +test "bdecode garbage" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "xz1234")); +} + +test "bdecode number" { + var a = std.testing.allocator; + var bval = try bdecodeBuf(a, "i123e"); + defer bval.deinit(a); + try std.testing.expectEqualDeep(BValue{ .int = 123 }, bval); +} + +test "bdecode number negative" { + var a = std.testing.allocator; + var bval = try bdecodeBuf(a, "i-123e"); + defer bval.deinit(a); + try std.testing.expectEqualDeep(BValue{ .int = -123 }, bval); +} + +test "bdecode number empty" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "ie")); +} + +test "bdecode number just sign" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "i-e")); +} + +test "bdecode number no end" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "i123123671283")); +} + +test "bdecode number out of range" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "i9223372036854775808e")); +} + +test "bdecode string" { + var a = std.testing.allocator; + var bval = try bdecodeBuf(a, "5:hello"); + defer bval.deinit(a); + try std.testing.expectEqualDeep(BValue{ .string = "hello" }, bval); +} +test "bdecode string too short" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "5:hell")); +} + +test "bdecode list" { + var a = std.testing.allocator; + var bval = try bdecodeBuf(a, "l5:hello5:worlde"); + defer bval.deinit(a); + try std.testing.expectEqual(@as(usize, 2), bval.list.items.len); + try std.testing.expectEqualStrings("hello", bval.list.items[0].string); + try std.testing.expectEqualStrings("world", bval.list.items[1].string); +} + +test "invalid list" { + var a = std.testing.allocator; + try std.testing.expectError(error.EndOfStream, bdecodeBuf(a, "l5:hello5:world")); // missing end +} + +test "dict" { + var a = std.testing.allocator; + var bval = try bdecodeBuf(a, "d5:hello5:worlde"); + defer bval.deinit(a); + var v = bval.dict.getPtr("hello") orelse return error.TestExpectedNotNull; + try std.testing.expectEqualStrings("world", v.string); +} + +test "invalid dict no value" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "d5:hello5:world2:hie")); +} + +test "invalid dict wrong key type" { + var a = std.testing.allocator; + try std.testing.expectError(error.Malformatted, bdecodeBuf(a, "di32e5:helloe")); +} + +test "nested structure" { + var a = std.testing.allocator; + var bval = try bdecodeBuf(a, "d5:hello5:world2:hili123ei456el4:nesteee"); + defer bval.deinit(a); + var v = bval.dict.getPtr("hello") orelse return error.TestExpectedNotNull; + try std.testing.expectEqualStrings("world", v.string); + var v2 = bval.dict.getPtr("hi") orelse return error.TestExpectedNotNull; + try std.testing.expectEqualDeep(v2.*.list.items[0], BValue{ .int = 123 }); + try std.testing.expectEqualDeep(v2.*.list.items[1], BValue{ .int = 456 }); + try std.testing.expectEqualStrings("nest", v2.*.list.items[2].list.items[0].string); +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..fa8a1b6 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,7 @@ +const std = @import("std"); + +pub fn main() !void {} + +test { + _ = @import("bencode.zig"); +} |