//! https://wiki.theory.org/BitTorrentSpecification#Metainfo_File_Structure const std = @import("std"); const bencode = @import("bencode.zig"); const MetaInfo = @This(); pub const Error = (error{Malformatted} || std.mem.Allocator.Error); pub const Info = struct { pub const File = struct { name: []const u8, length: u64, path: []const u8, }; piece_length: u64, pieces: []const u8, files: []File, pub fn parse(a: std.mem.Allocator, b: bencode.BValue) Error!Info { var d = b.asDict() catch return error.Malformatted; const pl = d.get("piece length") orelse return error.Malformatted; const pp = d.get("pieces") orelse return error.Malformatted; var files = std.ArrayList(File).init(a); defer files.deinit(); if (d.get("files")) |f| { // multi-file mode const l = f.asList() catch return error.Malformatted; for (l.items) |fi| { const fd = fi.asDict() catch return error.Malformatted; const fin = fd.get("name") orelse return error.Malformatted; const fl = fd.get("length") orelse return error.Malformatted; const fp = fd.get("path") orelse return error.Malformatted; try files.append(.{ .name = fin.asString() catch return error.Malformatted, .length = fl.asInt(u64) catch return error.Malformatted, .path = fp.asString() catch return error.Malformatted, }); } } else { // single-file mode const fin = d.get("name") orelse return error.Malformatted; const fl = d.get("length") orelse return error.Malformatted; try files.append(.{ .name = fin.asString() catch return error.Malformatted, .length = fl.asInt(u64) catch return error.Malformatted, .path = fin.asString() catch return error.Malformatted, // just use the file name as path }); } return .{ .piece_length = pl.asInt(u64) catch return error.Malformatted, .pieces = pp.asString() catch return error.Malformatted, .files = try files.toOwnedSlice(), }; } pub fn deinit(self: *Info, a: std.mem.Allocator) void { a.free(self.files); } }; info: Info, announce: []const u8, pub fn parse(a: std.mem.Allocator, b: bencode.BValue) Error!MetaInfo { // TODO diagnostics var d = b.asDict() catch return error.Malformatted; const i = d.get("info") orelse return error.Malformatted; const an = d.get("announce") orelse return error.Malformatted; return .{ .info = try Info.parse(a, i), .announce = an.asString() catch return error.Malformatted, }; } pub fn deinit(self: *MetaInfo, a: std.mem.Allocator) void { self.info.deinit(a); } test "sample" { const a = std.testing.allocator; const sample_str = @embedFile("sample.torrent"); var b = try bencode.bdecodeBuf(a, sample_str); defer b.deinit(a); var mi = try MetaInfo.parse(a, b); defer mi.deinit(a); try std.testing.expectEqualStrings("http://bittorrent-test-tracker.codecrafters.io/announce", mi.announce); try std.testing.expectEqual(@as(usize, 1), mi.info.files.len); try std.testing.expectEqualStrings("sample.txt", mi.info.files[0].name); }