aboutsummaryrefslogtreecommitdiff
path: root/src/metainfo.zig
blob: a202b0e0f3937d41af98d66a4c432cf091a2ed20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//! 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);
}