From f6beb617bdfcde64a4a6f3b426437410dd4ec49b Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sun, 16 Jun 2024 17:03:13 +0200 Subject: [PATCH] Initial working bencode and torrent parser --- .gitignore | 245 ++++++++++++++++++++++++++++ build.zig | 71 +++++++++ build.zig.zon | 6 + src/bencode.zig | 340 +++++++++++++++++++++++++++++++++++++++ src/client.zig | 0 src/main.zig | 20 +++ src/network.zig | 55 +++++++ src/torrent.zig | 300 +++++++++++++++++++++++++++++++++++ src/tracker.zig | 35 ++++ test/debian.torrent | Bin 0 -> 50790 bytes test/rocky.torrent | Bin 0 -> 13625 bytes test/simple.torrent | 1 + tools/bencode.el | 377 ++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 1450 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/bencode.zig create mode 100644 src/client.zig create mode 100644 src/main.zig create mode 100644 src/network.zig create mode 100644 src/torrent.zig create mode 100644 src/tracker.zig create mode 100644 test/debian.torrent create mode 100644 test/rocky.torrent create mode 100644 test/simple.torrent create mode 100644 tools/bencode.el diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8518230 --- /dev/null +++ b/.gitignore @@ -0,0 +1,245 @@ +# Created by https://www.toptal.com/developers/gitignore/api/zig,emacs,jetbrains+all,linux,windows,macos,vim +# Edit at https://www.toptal.com/developers/gitignore?templates=zig,emacs,jetbrains+all,linux,windows,macos,vim + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### zig ### +# Zig programming language + +zig-cache/ +zig-out/ +build/ +build-*/ +docgen_tmp/ + +# End of https://www.toptal.com/developers/gitignore/api/zig,emacs,jetbrains+all,linux,windows,macos,vim diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..fd79589 --- /dev/null +++ b/build.zig @@ -0,0 +1,71 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "zephyr", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + exe.linkSystemLibrary("c"); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..4a97a49 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,6 @@ +.{ + .name = "zephyr", + .version = "0.0.1", + .paths = .{""}, + .dependencies = .{}, +} diff --git a/src/bencode.zig b/src/bencode.zig new file mode 100644 index 0000000..9bb5a27 --- /dev/null +++ b/src/bencode.zig @@ -0,0 +1,340 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Reader = std.io.Reader; +const StringHashMap = std.StringHashMap; + +pub const BType = union(enum) { + Integer: i64, + String: []const u8, + List: []const BType, + Dict: StringHashMap(BType), + + pub fn get_as(self: BType, comptime tag: std.meta.Tag(BType), key: []const u8) !std.meta.TagPayload(BType, tag) { + if (self != BType.Dict) return error.NoDict; + + const val = self.Dict.get(key) orelse return error.KeyNotFound; + + if (val != tag) return error.BTypeMismatch; + + return @field(val, @tagName(tag)); + } + + pub fn format(value: BType, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + switch (value) { + .Integer => { + try writer.print("{}", .{value.Integer}); + }, + .String => { + try writer.print("{s}", .{value.String}); + }, + .List => { + try writer.print("[\n", .{}); + for (value.List) |e| { + try writer.print("\t", .{}); + try format(e, fmt, options, writer); + } + try writer.print("\n]", .{}); + }, + .Dict => { + try writer.print("{{\n", .{}); + var it = value.Dict.keyIterator(); + while (it.next()) |k| { + try writer.print("\t", .{}); + try writer.print("{s}:", .{k.*}); + const val = value.Dict.get(k.*).?; + try format(val, fmt, options, writer); + } + try writer.print("\n}}", .{}); + }, + } + } +}; + +pub const BParse = struct { + const Self = @This(); + + alloc: ArenaAllocator, + value: BType, + + pub fn get_as(self: Self, comptime tag: std.meta.Tag(BType), key: []const u8) !std.meta.TagPayload(BType, tag) { + return self.value.get_as(tag, key); + } + + pub fn deinit(self: Self) void { + self.alloc.deinit(); + } +}; + +const ParseError = error{ EndOfStream, FormatError }; + +pub fn parse(allocator: Allocator, reader: anytype) anyerror!BParse { + var ally = std.heap.ArenaAllocator.init(allocator); + errdefer ally.deinit(); + + const bparse = try parseInternal(ally.allocator(), reader); + + return BParse{ .alloc = ally, .value = bparse }; +} + +fn parseInternal(allocator: Allocator, reader: anytype) anyerror!BType { + while (nextByte(reader)) |next| { + return try parseInternalNext(allocator, next, reader); + } + return ParseError.EndOfStream; +} + +fn parseInternalNext(allocator: Allocator, next: u8, reader: anytype) anyerror!BType { + switch (next) { + 'i' => { + const res = try parse_bint(reader); + return BType{ .Integer = res }; + }, + 'l' => { + const res = try parse_blist(allocator, reader); + return BType{ .List = res }; + }, + 'd' => { + const res = try parse_bdict(allocator, reader); + return BType{ .Dict = res }; + }, + '0'...'9' => { + const res = try parse_bstring(allocator, next, reader); + return BType{ .String = res }; + }, + else => { + return ParseError.FormatError; + }, + } + + unreachable; +} + +fn parse_blist(allocator: Allocator, reader: anytype) anyerror![]const BType { + var buf = std.ArrayList(BType).init(allocator); + errdefer buf.deinit(); + + while (nextByte(reader)) |next| { + switch (next) { + 'e' => break, + else => { + const el = try parseInternalNext(allocator, next, reader); + try buf.append(el); + }, + } + } + + return buf.toOwnedSlice(); +} + +fn parse_bdict(allocator: Allocator, reader: anytype) anyerror!StringHashMap(BType) { + var map = StringHashMap(BType).init(allocator); + errdefer map.deinit(); + + while (nextByte(reader)) |next| { + switch (next) { + 'e' => { + break; + }, + else => { + const key = try parse_bstring(allocator, next, reader); + + const value_next = nextByte(reader) orelse return error.FormatError; + const value = try parseInternalNext(allocator, value_next, reader); + + try map.put(key, value); + }, + } + } + + return map; +} + +/// Parses a bencode string into a `u8` slice +/// +/// Allocates a buffer for the string. Caller owns the memory. +fn parse_bstring(allocator: Allocator, first: u8, reader: anytype) ![]const u8 { + const len = try parse_bstring_len(allocator, first, reader); + + var buf = try allocator.alloc(u8, len); + + for (0..len) |i| { + const next = nextByte(reader) orelse return ParseError.FormatError; + buf[i] = next; + } + return buf; +} + +/// Tries to parse the length specifier for a bencode string +/// +/// Owns the memory. Only temporary allocates for parsing and deallocates when +/// finished. +fn parse_bstring_len(allocator: Allocator, first: u8, reader: anytype) !usize { + // `first` already consumed from reader at that point so we need to add it + var ally = ArenaAllocator.init(allocator); + defer ally.deinit(); + + var buf = std.ArrayList(u8).init(ally.allocator()); + + try buf.append(first); + + while (nextByte(reader)) |next| { + switch (next) { + ':' => { + break; + }, + '0'...'9' => { + try buf.append(next); + }, + else => { + return ParseError.FormatError; + }, + } + } + + const tmp = try buf.toOwnedSlice(); + return try std.fmt.parseUnsigned(usize, tmp, 10); +} + +fn parse_bint(reader: anytype) !i64 { + var parse_buf: [20]u8 = undefined; // -9223372036854775808 to 9223372036854775807 + var len: usize = 0; + + while (nextByte(reader)) |next| { + switch (next) { + '0'...'9' => { + parse_buf[len] = next; + len += 1; + }, + + 'e' => { + return try std.fmt.parseInt(i64, parse_buf[0..len], 10); + }, + + else => { + return ParseError.FormatError; + }, + } + } + + return ParseError.FormatError; +} + +fn nextByte(reader: anytype) ?u8 { + return reader.readByte() catch { + std.log.debug("Parse reached end of stream", .{}); + return null; + }; +} + +test "parse int i322323e" { + const bencode = "i322323e"; + var stream = std.io.fixedBufferStream(bencode); + + const res = try parse(std.testing.allocator, stream.reader()); + defer res.deinit(); + + try std.testing.expectEqual(@as(i64, 322323), res.value.Integer); +} + +test "parse string 3:abc" { + const bencode = "3:abc"; + var stream = std.io.fixedBufferStream(bencode); + + const res = try parse(std.testing.allocator, stream.reader()); + defer res.deinit(); + + try std.testing.expectEqualStrings("abc", res.value.String); +} + +test "parse invalid int i12" { + const bencode = "i12"; + var stream = std.io.fixedBufferStream(bencode); + + const res = parse(std.testing.allocator, stream.reader()); + + try std.testing.expectError(error.FormatError, res); +} + +test "parse list l4:spam4:eggse" { + const bencode = "l4:spam4:eggse"; + + var stream = std.io.fixedBufferStream(bencode); + + const res = try parse(std.testing.allocator, stream.reader()); + defer res.deinit(); + + try std.testing.expect(res.value.List.len == 2); + try std.testing.expectEqualStrings("spam", res.value.List[0].String); + try std.testing.expectEqualStrings("eggs", res.value.List[1].String); +} + +test "parse list l4:spami322323e4:eggse" { + const bencode = "l4:spami322323e4:eggse"; + + var stream = std.io.fixedBufferStream(bencode); + + const res = try parse(std.testing.allocator, stream.reader()); + defer res.deinit(); + + try std.testing.expect(res.value.List.len == 3); + try std.testing.expectEqualStrings("spam", res.value.List[0].String); + try std.testing.expectEqual(@as(i64, 322323), res.value.List[1].Integer); + try std.testing.expectEqualStrings("eggs", res.value.List[2].String); +} + +test "parse list l4:spamli322323e4:fishe4:eggse" { + const bencode = "l4:spamli322323e4:fishe4:eggse"; + + var stream = std.io.fixedBufferStream(bencode); + + const res = try parse(std.testing.allocator, stream.reader()); + defer res.deinit(); + + try std.testing.expect(res.value.List.len == 3); + try std.testing.expectEqualStrings("spam", res.value.List[0].String); + + //nested list + try std.testing.expect(res.value.List[1].List.len == 2); + try std.testing.expectEqual(@as(i64, 322323), res.value.List[1].List[0].Integer); + try std.testing.expectEqualStrings("fish", res.value.List[1].List[1].String); + + try std.testing.expectEqualStrings("eggs", res.value.List[2].String); +} + +test "parse map d4:spamli322323e4:fishe4:eggsi1234e ({spam:[322323,fish], eggs:1234})" { + const bencode = "d4:spamli322323e4:fishe4:eggsi1234e"; + + var stream = std.io.fixedBufferStream(bencode); + + const res = try parse(std.testing.allocator, stream.reader()); + defer res.deinit(); + + try std.testing.expect(res.value.Dict.count() == 2); + try std.testing.expect(res.value.Dict.contains("spam")); + try std.testing.expect(res.value.Dict.contains("eggs")); + + try std.testing.expectEqual(@as(i64, 322323), res.value.Dict.get("spam").?.List[0].Integer); + try std.testing.expectEqualStrings("fish", res.value.Dict.get("spam").?.List[1].String); + try std.testing.expectEqual(@as(i64, 1234), res.value.Dict.get("eggs").?.Integer); +} + +test "parse invalid string 3:ab" { + const bencode = "3:ab"; + var stream = std.io.fixedBufferStream(bencode); + + const res = parse(std.testing.allocator, stream.reader()); + + try std.testing.expectError(error.FormatError, res); +} + +test "parse debian torrent" { + const bencode = try std.fs.cwd() + .openFile("test/simple.torrent", .{}); + defer bencode.close(); + + var buffered_reader = std.io.bufferedReader(bencode.reader()); + + const res = try parse(std.testing.allocator, buffered_reader.reader()); + defer res.deinit(); +} diff --git a/src/client.zig b/src/client.zig new file mode 100644 index 0000000..e69de29 diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..ea387c6 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const torrent = @import("torrent.zig"); +const bencode = @import("bencode.zig"); +const tracker = @import("tracker.zig"); +const network = @import("network.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + const t = try torrent.Torrent.parse(allocator, args[1]); + defer t.deinit(); + + const outw = std.io.getStdOut().writer(); + try outw.print("{}", .{t}); +} diff --git a/src/network.zig b/src/network.zig new file mode 100644 index 0000000..4abd9a8 --- /dev/null +++ b/src/network.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("arpa/inet.h"); + @cInclude("netdb.h"); +}); + +// roughly after +// https://beej.us/guide/bgnet/html/split/system-calls-or-bust.html#getaddrinfoprepare-to-launch +fn printip() !void { + + // equivalent to beej's `memset(&hints, 0, sizeof hints);` + // zeroing out the struct + var hints: c.addrinfo = std.mem.zeroes(c.addrinfo); + hints.ai_family = c.AF_UNSPEC; + hints.ai_socktype = c.SOCK_DGRAM; + hints.ai_flags = 0; + hints.ai_protocol = 0; + + var results: ?*c.addrinfo = null; + + const res = c.getaddrinfo("example.com", "443", &hints, &results); + defer if (results != null) c.freeaddrinfo(results); + if (res != 0) return error.UnableToResolve; + + var ip_buf: [c.INET6_ADDRSTRLEN]u8 = undefined; + + var rp = results; + while (rp) |addr| : (rp = rp.?.ai_next) { + switch (addr.ai_family) { + c.AF_INET => { + const ipv4: *c.sockaddr_in = @alignCast(@ptrCast(addr.ai_addr)); + const ip = c.inet_ntop(addr.ai_family, &ipv4.sin_addr, &ip_buf, c.INET_ADDRSTRLEN); + if (ip == null) return error.UntranslatableIP; + + std.debug.print("Addr IPv4: {s}\n", .{ip}); + }, + + c.AF_INET6 => { + const ipv6: *c.sockaddr_in6 = @ptrCast(@alignCast(addr.ai_addr)); + const ip = c.inet_ntop(addr.ai_family, &ipv6.sin6_addr, &ip_buf, c.INET6_ADDRSTRLEN); + if (ip == null) return error.UntranslatableIP; + + std.debug.print("Addr IPv6: {s}\n", .{ip}); + }, + + else => { + return error.UnknownFamily; + }, + } + } +} + +test "print ip" { + try printip(); +} diff --git a/src/torrent.zig b/src/torrent.zig new file mode 100644 index 0000000..8da6f9e --- /dev/null +++ b/src/torrent.zig @@ -0,0 +1,300 @@ +const std = @import("std"); +const bencode = @import("bencode.zig"); +const BType = bencode.BType; +const Allocator = std.mem.Allocator; + +const TorrentError = error{ InvalidTorrent, MissingEntry, AllocError }; + +pub const Torrent = struct { + const Tier = std.ArrayList([]const u8); + + const File = struct { // zig fmt: off + length: u64, + path: std.ArrayList([]const u8) + }; + + const Info = struct { // zig fmt: off + name: []const u8, + piece_length: u64, + pieces: [][20]u8, + Type: union(enum) { + Single: struct { length: u64 }, + Multi: struct { files: std.ArrayList(File) } + } + }; + + announce: []const u8, + announce_list: std.ArrayList(Tier), + comment: ?[]const u8, + info: Info, + allocator: Allocator, + + pub fn parse(allocator: Allocator, path: []u8) TorrentError!Torrent { + var torrent = Torrent{ // zig fmt: off + .announce = &[_]u8{}, + .announce_list = std.ArrayList(Tier).init(allocator), + .comment = null, + .info = undefined, + .allocator = allocator + }; + errdefer torrent.deinit(); + + const bparse: bencode.BParse = becode_decode(allocator, path) catch return error.InvalidTorrent; + defer bparse.deinit(); + + if (bparse.value != BType.Dict) return error.InvalidTorrent; + + + const announce = bparse.get_as(BType.String, "announce") catch return error.InvalidTorrent; + torrent.announce = allocator.dupe(u8, announce) catch return error.AllocError; + + try parse_announce_list(allocator, bparse, &torrent); + + if(bparse.value.Dict.contains("comment")) { + const comment = bparse.get_as(BType.String, "comment") catch return error.InvalidTorrent; + torrent.comment = allocator.dupe(u8, comment) catch return error.AllocError; + } + + const info = bparse.value.Dict.get("info") orelse return error.InvalidTorrent; + if (info != BType.Dict) return error.InvalidTorrent; + + try parse_info_common(allocator, info, &torrent); + + if (info.Dict.contains("length")) { + torrent.info.Type = .{ .Single = .{ + .length = @intCast(info.get_as(BType.Integer, "length") catch return error.InvalidTorrent), + } }; + } else { + torrent.info.Type = .{ .Multi = .{ .files = try parse_info_multifile(allocator, info) } }; + } + + return torrent; + } + + fn becode_decode(allocator: Allocator, path: []u8) !bencode.BParse { + const torrent_file = try std.fs.Dir.openFile(std.fs.cwd(), path, .{}); + defer torrent_file.close(); + + var buffered_reader = std.io.bufferedReader(torrent_file.reader()); + + const bparse = try bencode.parse(allocator, buffered_reader.reader()); + errdefer bparse.deinit(); + + return bparse; + } + + fn parse_announce_list(allocator: Allocator, bparse: bencode.BParse, torrent: *Torrent) !void { + if (!bparse.value.Dict.contains("announce-list")) return; + + const announce_list = bparse.get_as(BType.List, "announce-list") catch return error.InvalidTorrent; + + for (announce_list) |tier_list| { + if (tier_list != BType.List) return error.InvalidTorrent; + if (tier_list.List.len == 0) continue; + + var tier = Tier.init(allocator); + + for (tier_list.List) |tracker| { + if (tracker != BType.String) return error.InvalidTorrent; + tier.append(allocator.dupe(u8, tracker.String) catch return error.AllocError) catch return error.AllocError; + } + + torrent.announce_list.append(tier) catch return error.AllocError; + } + } + + fn parse_info_common(allocator: Allocator, info: BType, torrent: *Torrent) !void { + const name = info.get_as(BType.String, "name") catch return error.InvalidTorrent; + torrent.info.name = allocator.dupe(u8, name) catch return error.AllocError; + + const piece_length = info.get_as(BType.Integer, "piece length") catch return error.InvalidTorrent; + torrent.info.piece_length = @intCast(piece_length); + + // pieces are 20-byte SHA-1 hashes of file pieces + const info_pieces = info.get_as(BType.String, "pieces") catch return error.InvalidTorrent; + + const info_pieces_len = info_pieces.len / 20; + + torrent.info.pieces = allocator.alloc([20]u8, info_pieces_len) catch return error.AllocError; + + for (0..info_pieces_len) |i| { + @memcpy(&torrent.info.pieces[i], info_pieces[i .. i + 20]); + } + } + + fn parse_info_multifile(allocator: Allocator, info: BType) !std.ArrayList(File) { + var files = std.ArrayList(File).init(allocator); + + const info_files = info.get_as(BType.List, "files") catch return error.InvalidTorrent; + + for (info_files) |info_file| { + if (info_file != BType.Dict) return error.InvalidTorrent; + + var torrent_file = File{ .length = undefined, .path = std.ArrayList([]const u8).init(allocator) }; + + const file_length = info_file.get_as(BType.Integer, "length") catch return error.InvalidTorrent; + torrent_file.length = @intCast(file_length); + + const file_path = info_file.get_as(BType.List, "path") catch return error.InvalidTorrent; + for (file_path) |p| { + if (p != BType.String) return error.InvalidTorrent; + const p_dupe = allocator.dupe(u8, p.String) catch return error.AllocError; + torrent_file.path.append(p_dupe) catch return error.AllocError; + } + + files.append(torrent_file) catch return error.AllocError; + } + + return files; + } + + pub fn format(value: Torrent, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { + try writer.print( + \\Torrent {{ + \\ .announce = {s} + \\ + , .{value.announce}); + + try writer.print(" .announce-list = [", .{}); + for (value.announce_list.items, 0..) |tier, idx| { + try writer.print("\n [", .{}); + for (tier.items, 0..) |tracker, idx2| { + try writer.print("{s}", .{tracker}); + if (idx2 < tier.items.len - 1) try writer.print(", ", .{}); + } + try writer.print("]", .{}); + + if (idx < value.announce_list.items.len - 1) try writer.print(", ", .{}); + } + try writer.print("]\n", .{}); + + try writer.print(" .comment = {?s}\n", .{value.comment}); + + try writer.print( + \\ .info = {{ + \\ .name = {s} + \\ .piece_length = {} + \\ + , .{ value.info.name, value.info.piece_length }); + + switch (value.info.Type) { + .Multi => |multi| { + try writer.print(" .files = [\n", .{}); + + for (multi.files.items) |file| { + try writer.print(" {{.length = {}, .path = [", .{file.length}); + for (0..file.path.items.len) |i| { + try writer.print("{s}", .{file.path.items[i]}); + if (i < file.path.items.len - 1) { + try writer.print(", ", .{}); + } + } + try writer.print("]}}\n", .{}); + } + + try writer.print(" ]\n", .{}); + }, + .Single => |single| { + try writer.print(" .length = {}\n", .{single.length}); + }, + } + + try writer.print(" .pieces = \n", .{}); + for (value.info.pieces) |p| { + try writer.print(" {}\n", .{std.fmt.fmtSliceHexUpper(&p)}); + } + + try writer.print( + \\ }} + \\}} + \\ + , .{}); + } + + pub fn deinit(self: Torrent) void { + self.allocator.free(self.announce); + if(self.comment) |comment| self.allocator.free(comment); + self.allocator.free(self.info.name); + self.allocator.free(self.info.pieces); + + for (self.announce_list.items) |tier| { + for (tier.items) |tracker| { + self.allocator.free(tracker); + } + + tier.deinit(); + } + self.announce_list.deinit(); + + switch (self.info.Type) { + .Multi => |multi| { + for (multi.files.items) |file| { + for (file.path.items) |p| { + self.allocator.free(p); + } + file.path.deinit(); + } + self.info.Type.Multi.files.deinit(); + }, + .Single => {}, + } + } +}; + +test "parse simple torrent" { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try std.fs.realpath("test/simple.torrent", &buf); + + const res = try Torrent.parse(std.testing.allocator, path); + defer res.deinit(); + + try std.testing.expectEqualStrings("http://example.com", res.announce); + try std.testing.expectEqualStrings("simple", res.info.name); + try std.testing.expectEqual(@as(u64, 7), res.info.Type.Single.length); + try std.testing.expectEqual(@as(u64, 262144), res.info.piece_length); +} + +test "parse multifile real torrent" { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try std.fs.realpath("test/rocky.torrent", &buf); + + const res = try Torrent.parse(std.testing.allocator, path); + defer res.deinit(); + + try std.testing.expectEqualStrings("http://linuxtracker.org:2710/00000000000000000000000000000000/announce", res.announce); + try std.testing.expectEqualStrings("Rocky-8.10-x86_64-minimal", res.info.name); + + try std.testing.expectEqual(4, res.info.Type.Multi.files.items.len); + + try std.testing.expectEqual(@as(u64, 1502), res.info.Type.Multi.files.items[0].length); + try std.testing.expectEqual(1, res.info.Type.Multi.files.items[0].path.items.len); + try std.testing.expectEqualStrings("CHECKSUM", res.info.Type.Multi.files.items[0].path.items[0]); + + try std.testing.expectEqual(@as(u64, 2694053888), res.info.Type.Multi.files.items[1].length); + try std.testing.expectEqual(1, res.info.Type.Multi.files.items[1].path.items.len); + try std.testing.expectEqualStrings("Rocky-8.10-x86_64-minimal.iso", res.info.Type.Multi.files.items[1].path.items[0]); + + try std.testing.expectEqual(@as(u64, 156), res.info.Type.Multi.files.items[2].length); + try std.testing.expectEqual(1, res.info.Type.Multi.files.items[2].path.items.len); + try std.testing.expectEqualStrings("Rocky-8.10-x86_64-minimal.iso.CHECKSUM", res.info.Type.Multi.files.items[2].path.items[0]); + + try std.testing.expectEqual(@as(u64, 103171), res.info.Type.Multi.files.items[3].length); + try std.testing.expectEqual(1, res.info.Type.Multi.files.items[3].path.items.len); + try std.testing.expectEqualStrings("Rocky-8.10-x86_64-minimal.iso.manifest", res.info.Type.Multi.files.items[3].path.items[0]); + + try std.testing.expectEqual(@as(u64, 4194304), res.info.piece_length); +} + +test "parse singlefile real torrent" { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + const path = try std.fs.realpath("test/debian.torrent", &buf); + + const res = try Torrent.parse(std.testing.allocator, path); + defer res.deinit(); + + try std.testing.expectEqualStrings("http://bttracker.debian.org:6969/announce", res.announce); + try std.testing.expectEqualStrings("debian-12.5.0-amd64-netinst.iso", res.info.name); + + try std.testing.expectEqual(@as(u64, 659554304), res.info.Type.Single.length); + try std.testing.expectEqual(@as(u64, 262144), res.info.piece_length); +} diff --git a/src/tracker.zig b/src/tracker.zig new file mode 100644 index 0000000..e89a441 --- /dev/null +++ b/src/tracker.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const torrent = @import("torrent.zig"); + +const Torrent = torrent.Torrent; + +const Peer = struct { + ip: std.net.Address, + port: u16, +}; + +fn get_peers(allocator: std.mem.Allocator, t: Torrent) !void { + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + + const stream: std.net.Stream = try std.net.tcpConnectToHost(arena.allocator(), t.announce, 6969); + defer stream.close(); + + try stream.writeAll( + \\GET /announce?info_hash=%D9%BE%BA%F7%CD4%F7v%DC%D7_4%E9%F6%9B%E7G%1B%DD%C6%F8%D7%CD%F9o%DEZ%D3~%B6&peer_id=1&uploaded=0&downloaded=0 HTTP/1.1 + \\Host: bttracker.debian.org + \\ + \\ + ); + + var buf: [20]u8 = undefined; + const res = try stream.reader().read(&buf); + + std.log.err("Result {any}", .{res}); +} + +test "something" { + const t = Torrent{ .announce = "bttracker.debian.org", .allocator = undefined, .info = undefined }; + + try get_peers(std.testing.allocator, t); +} diff --git a/test/debian.torrent b/test/debian.torrent new file mode 100644 index 0000000000000000000000000000000000000000..a935008a0d91f7989cb5232898eb2b0ff18fd429 GIT binary patch literal 50790 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

?lxGLu2Xg!@$e(Isq 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 N&xc_rrx6SQ{9mR!|3Lr% literal 0 HcmV?d00001 diff --git a/test/simple.torrent b/test/simple.torrent new file mode 100644 index 0000000..ba55a3c --- /dev/null +++ b/test/simple.torrent @@ -0,0 +1 @@ +d8:announce18:http://example.com7:comment7:Comment10:created by13:mktorrent 1.113:creation datei1712361951e4:infod6:lengthi7e4:name6:simple12:piece lengthi262144e6:pieces20: ٨&y(9Aee \ No newline at end of file diff --git a/tools/bencode.el b/tools/bencode.el new file mode 100644 index 0000000..100529d --- /dev/null +++ b/tools/bencode.el @@ -0,0 +1,377 @@ +;;; bencode.el --- Bencode encoding / decoding -*- lexical-binding: t; -*- + +;; This is free and unencumbered software released into the public domain. + +;; Author: Christopher Wellons +;; URL: https://github.com/skeeto/emacs-bencode +;; Version: 1.0 +;; Package-Requires: ((emacs "24.4")) + +;;; Commentary: + +;; This package provides a strict and robust [bencode][bencode] +;; encoder and decoder. Encoding is precise, taking into account +;; character encoding issues. As such, the encoder always returns +;; unibyte data intended to be written out as raw binary data without +;; additional character encoding. When encoding strings and keys, +;; UTF-8 is used by default. The decoder strictly valides its input, +;; rejecting invalid inputs. + +;; The API entrypoints are: +;; * `bencode-encode' +;; * `bencode-encode-to-buffer' +;; * `bencode-decode' +;; * `bencode-decode-from-buffer' + +;;; Code: + +(require 'cl-lib) + +(define-error 'bencode "Bencode error") +(define-error 'bencode-unsupported-type "Type cannot be encoded" 'bencode) +(define-error 'bencode-invalid-key "Not a valid dictionary key" 'bencode) +(define-error 'bencode-invalid-plist "Plist is invalid" 'bencode) +(define-error 'bencode-invalid-byte "Invalid input byte" 'bencode) +(define-error 'bencode-overflow "Integer too large" 'bencode) +(define-error 'bencode-end-of-file "End of file during parsing" + '(bencode end-of-file)) + +(defsubst bencode--int (object) + "Encode OBJECT as an integer into the current buffer." + (insert "i" (number-to-string object) "e")) + +(defsubst bencode--string (object coding-system) + "Encode OBJECT as a string into the current buffer." + (if (multibyte-string-p object) + (let ((encoded (encode-coding-string object coding-system :nocopy))) + (insert (number-to-string (length encoded)) ":" encoded)) + (insert (number-to-string (length object)) ":" object))) + +(defsubst bencode--hash-table-entries (object coding-system) + "Return a list of key-sorted entries in OBJECT with encoded keys." + (let ((entries ())) + (maphash (lambda (key value) + (cond + ((multibyte-string-p key) + (let ((encoded (encode-coding-string + key coding-system :nocopy))) + (push (cons encoded value) entries))) + ((stringp key) + (push (cons key value) entries)) + ((signal 'bencode-invalid-key key)))) + object) + (cl-sort entries #'string< :key #'car))) + +(defsubst bencode--plist-entries (object coding-system) + "Return a list of key-sorted entries in OBJECT with encoded keys." + (let ((plist object) + (entries ())) + (while plist + (let ((key (pop plist))) + (unless (keywordp key) + (signal 'bencode-invalid-key key)) + (when (null plist) + (signal 'bencode-invalid-plist object)) + (let ((name (substring (symbol-name key) 1)) + (value (pop plist))) + (if (multibyte-string-p name) + (let ((encoded (encode-coding-string + name coding-system :nocopy))) + (push (cons encoded value) entries)) + (push (cons name value) entries))))) + (cl-sort entries #'string< :key #'car))) + +(cl-defun bencode-encode (object &key (coding-system 'utf-8)) + "Return a unibyte string encoding OBJECT with bencode. + +:coding-system -- coding system for encoding strings into byte strings (utf-8) + +Supported types: +* Integer +* Multibyte and unibyte strings +* List of supported types +* Vector of supproted types (encodes to list) +* Hash table with string keys (encodes to dictionary) +* Plist with keyword symbol keys (encodes to dictionary) + +When multibyte strings are encountered either as values or dictionary +keys, they are encoded with the specified coding system (default: +UTF-8). The same coding system must be used when decoding. + +Possible error signals: +* bencode-unsupported-type +* bencode-invalid-key +* bencode-invalid-plist + +This function is not recursive. It is safe to input very deeply +nested data structures." + (with-temp-buffer + (set-buffer-multibyte nil) + (bencode-encode-to-buffer object :coding-system coding-system) + (buffer-string))) + +(cl-defun bencode-encode-to-buffer (object &key (coding-system 'utf-8)) + "Like `bencode-encode' but to the current buffer at point." + (let ((stack (list (cons :new object)))) + (while stack + (let* ((next (car stack)) + (value (cdr next))) + (cl-case (car next) + ;; Start encoding a new, unexamined value + (:new + (pop stack) + (cond ((integerp value) + (bencode--int value)) + ((stringp value) + (bencode--string value coding-system)) + ((and (consp value) + (keywordp (car value))) + (insert "d") + (let ((entries (bencode--plist-entries value coding-system))) + (push (cons :dict entries) stack))) + ((listp value) + (insert "l") + (push (cons :list value) stack)) + ((vectorp value) + (insert "l") + (push (cons :vector (cons 0 value)) stack)) + ((hash-table-p value) + (insert "d") + (let ((entries (bencode--hash-table-entries + value coding-system))) + (push (cons :dict entries) stack))) + ((signal 'bencode-unsupported-type object)))) + ;; Continue encoding dictionary + ;; (:dict . remaining-dict) + (:dict + (if (null value) + (progn + (pop stack) + (insert "e")) + (let* ((entry (car value)) + (key (car entry))) + (insert (number-to-string (length key)) ":" key) + (setf (cdr next) (cdr value)) + (push (cons :new (cdr entry)) stack)))) + ;; Continue encoding list + ;; (:list . remaining-list) + (:list + (if (null value) + (progn + (pop stack) + (insert "e")) + (setf (cdr next) (cdr value)) + (push (cons :new (car value)) stack))) + ;; Continue encoding vector (as list) + ;; (:vector index . vector) + (:vector + (let ((i (car value)) + (v (cdr value))) + (if (= i (length v)) + (progn + (pop stack) + (insert "e")) + (setf (car value) (+ i 1)) + (push (cons :new (aref v i)) stack))))))))) + +(defsubst bencode--decode-int () + "Decode an integer from the current buffer at point." + (forward-char) + (let ((start (point))) + ;; Don't allow leading zeros + (if (eql (char-after) ?0) + ;; Unless the value *is* zero + (prog1 0 + (forward-char) + (unless (eql (char-after) ?e) + (signal 'bencode-invalid-byte + (cons (char-after) (point)))) + (forward-char)) + ;; Skip minus sign + (when (eql (char-after) ?-) + (forward-char) + ;; Negative zero not allowed + (when (eql (char-after) ?0) + (signal 'bencode-invalid-byte + (cons (char-after) (point))))) + ;; Check for empty integer + (when (eql ?e (char-after)) + (signal 'bencode-invalid-byte + (cons (char-after) (point)))) + ;; Skip over digits + (unless (re-search-forward "[^0-9]" nil :noerror) + (signal 'bencode-end-of-file (point))) + ;; Check for terminator + (unless (eql ?e (char-before)) + (signal 'bencode-invalid-byte + (cons (char-before) (point)))) + ;; Try to parse the digits + (let* ((string (buffer-substring start (point))) + (result (string-to-number string))) + (if (floatp result) + (signal 'bencode-overflow (cons string result)) + result))))) + +(defsubst bencode--decode-string (coding-system) + "Decode a string from the current buffer at point. + +Returns cons of (raw . decoded)." + (let ((start (point))) + (if (eql (char-after) ?0) + ;; Handle zero length as a special case + (progn + (forward-char) + (if (eql (char-after) ?:) + (prog1 '("" . "") + (forward-char)) + (signal 'bencode-invalid-byte + (cons (char-after) (point))))) + ;; Skip over length digits + (unless (re-search-forward "[^0-9]" nil :noerror) + (signal 'bencode-end-of-file (point))) + ;; Did we find a colon? + (unless (eql ?: (char-before)) + (signal 'bencode-invalid-byte + (cons (char-before) (point)))) + (let* ((length-string (buffer-substring start (- (point) 1))) + (length (string-to-number length-string))) + (when (floatp length) + (signal 'bencode-overflow + (cons length-string length))) + (when (> (+ (point) length) (point-max)) + (signal 'bencode-end-of-file (+ (point) length))) + (let ((string (buffer-substring (point) (+ (point) length)))) + (prog1 (cons string + (decode-coding-string string coding-system :nocopy)) + (forward-char length))))))) + +(defsubst bencode--to-plist (list) + "Convert a series of parsed dictionary entries into a plist." + (let ((plist ())) + (while list + (push (pop list) plist) + (push (intern (concat ":" (pop list))) plist)) + plist)) + +(defsubst bencode--to-hash-table (list) + "Convert a series of parsed dictionary entries into a hash table." + (let ((table (make-hash-table :test 'equal))) + (prog1 table + (while list + (let ((value (pop list)) + (key (pop list))) + (setf (gethash key table) value)))))) + +(cl-defun bencode-decode-from-buffer + (&key (list-type 'list) (dict-type 'plist) (coding-system 'utf-8)) + "Like `bencode-decode' but from the current buffer starting at point. + +The point is left where parsing finished. You may want to reject +inputs with data trailing beyond the point." + ;; Operations are pushed onto an operation stack. One operation is + ;; executed once per iteration. Some operations push multiple new + ;; operations onto the stack. When no more operations are left, + ;; return the remaining element from the value stack. + (let ((op-stack '(:read)) ; operations stack + (value-stack (list nil)) ; stack of parsed values + (last-key-stack ())) ; last key seen in top dictionary + (while op-stack + (cl-case (car op-stack) + ;; Figure out what type of value is to be read next and + ;; prepare stacks accordingly. + (:read + (pop op-stack) + (cl-case (char-after) + ((nil) (signal 'bencode-end-of-file (point))) + (?i (push (bencode--decode-int) (car value-stack))) + (?l (forward-char) + (push :list op-stack) + (push nil value-stack)) + (?d (forward-char) + (push :dict op-stack) + (push nil value-stack) + (push nil last-key-stack)) + ((?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9) + (push (cdr (bencode--decode-string coding-system)) + (car value-stack))) + (t (signal 'bencode-invalid-byte (point))))) + ;; Read a key and push it onto the list on top of the value stack + (:key + (pop op-stack) + (let* ((string (bencode--decode-string coding-system)) + (raw (car string)) + (key (cdr string)) + (last-key (car last-key-stack))) + (when last-key + (when (string= last-key raw) + (signal 'bencode-invalid-key (cons 'duplicate key))) + (when (string< raw last-key) + (signal 'bencode-invalid-key (list 'string> last-key raw)))) + (setf (car last-key-stack) raw) + (push key (car value-stack)))) + ;; End list, or queue operations to read another value + (:list + (if (eql (char-after) ?e) + (let ((result (nreverse (pop value-stack)))) + (forward-char) + (pop op-stack) + (if (eq list-type 'vector) + (push (vconcat result) (car value-stack)) + (push result (car value-stack)))) + (push :read op-stack))) + ;; End dict, or queue operations to read another entry + (:dict + (if (eql (char-after) ?e) + (let ((result (pop value-stack))) + (forward-char) + (pop op-stack) + (pop last-key-stack) + (if (eq dict-type 'hash-table) + (push (bencode--to-hash-table result) (car value-stack)) + (push (bencode--to-plist result) (car value-stack)))) + (push :read op-stack) + (push :key op-stack))))) + (caar value-stack))) + +(cl-defun bencode-decode + (string &key (list-type 'list) (dict-type 'plist) (coding-system 'utf-8)) + "Decode bencode data from STRING. + +:coding-system -- coding system for decoding byte strings (utf-8) +:dict-type -- target format for dictionaries (symbol: plist, hash-table) +:list-type -- target format for lists (symbol: list, vector) + +Input should generally be unibyte. Strings parsed as values and +keys will be decoded using the coding system indicated by the +given coding system (default: UTF-8). The same coding system +should be used as when encoding. There are never decoding errors +since Emacs can preserve arbitrary byte data across encoding and +decoding. See \"Text Representations\" in the Gnu Emacs Lisp +Reference Manual. + +Input is strictly validated and invalid inputs are rejected. This +includes dictionary key constraints. Dictionaries are decoded +into plists. Lists are decoded into lists. If an integer is too +large to store in an Emacs integer, the decoder will signal an +overlow error. Signals an error if STRING contains trailing data. + +Possible error signals: +* bencode-end-of-file +* bencode-invalid-key +* bencode-invalid-byte +* bencode-overflow + +This function is not recursive. It is safe to parse very deeply +nested inputs." + (with-temp-buffer + (insert string) + (setf (point) (point-min)) + (prog1 (bencode-decode-from-buffer :list-type list-type + :dict-type dict-type + :coding-system coding-system) + (when (< (point) (point-max)) + (signal 'bencode-invalid-byte (cons "Trailing data" (point))))))) + +(provide 'bencode) + +;;; bencode.el ends here