From 3bd3f432a95da405634cdbd2a662d79a3a5ba7af Mon Sep 17 00:00:00 2001 From: Christian Segundo Date: Sun, 11 Jun 2023 22:09:15 +0200 Subject: wip --- src/request.zig | 363 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 src/request.zig (limited to 'src/request.zig') diff --git a/src/request.zig b/src/request.zig new file mode 100644 index 0000000..950a1fc --- /dev/null +++ b/src/request.zig @@ -0,0 +1,363 @@ +const std = @import("std"); +const util = @import("util.zig"); + +const Method = enum { + // Torrent requests: Torrent action + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#31-torrent-action-requests + torrent_start, + torrent_start_now, + torrent_stop, + torrent_verify, + torrent_reannounce, + + // Torrent requests: Mutator + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#32-torrent-mutator-torrent-set + torrent_set, + + // Torrent requests: Torrent accessor + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#33-torrent-accessor-torrent-get + torrent_get, + + // Torrent requests: Torrent adding + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#34-adding-a-torrent + torrent_add, + + // Torrent requests: Torrent removing + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#35-removing-a-torrent + torrent_remove, + + // Torrent requests: Torrent moving + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#36-moving-a-torrent + torrent_set_location, + + // Torrent requests: Torrent moving + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#37-renaming-a-torrents-path + torrent_rename_path, + + // Session requests: Get + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#4--session-requests + session_get, + + // Session requests: Mutator + // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#411-mutators + session_set, + //session_stats, + + const Self = @This(); + + // Transmission RPC uses hyphens instead of underscores, this creates a + // table of method names using the same enum field names but with + // underscores. + pub const Fields = util.RPCFields(Self); + + pub fn str(self: Self) []const u8 { + return Fields[@enumToInt(self)]; + } + + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(self.str(), options, out_stream); + } +}; + +pub const SessionSetFields = struct { + alt_speed_down: ?usize = null, + alt_speed_enabled: ?bool = null, + alt_speed_time_begin: ?usize = null, + alt_speed_time_day: ?usize = null, + alt_speed_time_enabled: ?bool = null, + alt_speed_time_end: ?usize = null, + alt_speed_up: ?usize = null, + blocklist_enabled: ?bool = null, + blocklist_url: ?[]u8 = null, + cache_size_mb: ?usize = null, + default_trackers: ?[]u8 = null, + dht_enabled: ?bool = null, + download_dir: ?[]u8 = null, + download_dir_free_space: ?usize = null, + download_queue_enabled: ?bool = null, + download_queue_size: ?usize = null, + encryption: ?enum { + required, + preferred, + tolerated, + + const Self = @This(); + + pub const Fields = util.RPCFields(Self); + + pub fn str(self: Self) []const u8 { + return Fields[@enumToInt(self)]; + } + + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(self.str(), options, out_stream); + } + } = null, + idle_seeding_limit_enabled: ?bool = null, + idle_seeding_limit: ?usize = null, + incomplete_dir_enabled: ?bool = null, + incomplete_dir: ?[]u8 = null, + lpd_enabled: ?bool = null, + peer_limit_global: ?usize = null, + peer_limit_per_torrent: ?usize = null, + peer_port_random_on_start: ?bool = null, + peer_port: ?usize = null, + pex_enabled: ?bool = null, + port_forwarding_enabled: ?bool = null, + queue_stalled_enabled: ?bool = null, + queue_stalled_minutes: ?usize = null, + rename_partial_files: ?bool = null, + script_torrent_added_enabled: ?bool = null, + script_torrent_added_filename: ?[]u8 = null, + script_torrent_done_enabled: ?bool = null, + script_torrent_done_filename: ?[]u8 = null, + script_torrent_done_seeding_enabled: ?bool = null, + script_torrent_done_seeding_filename: ?[]u8 = null, + seed_queue_enabled: ?bool = null, + seed_queue_size: ?usize = null, + seedRatioLimit: ?f64 = null, + seedRatioLimited: ?bool = null, + speed_limit_down_enabled: ?bool = null, + speed_limit_down: ?usize = null, + speed_limit_up_enabled: ?bool = null, + speed_limit_up: ?usize = null, + start_added_torrents: ?bool = null, + trash_original_torrent_files: ?bool = null, + utp_enabled: ?bool = null, +}; + +pub const SessionGetFields = enum { + alt_speed_down, + alt_speed_enabled, + alt_speed_time_begin, + alt_speed_time_day, + alt_speed_time_enabled, + alt_speed_time_end, + alt_speed_up, + blocklist_enabled, + blocklist_size, + blocklist_url, + cache_size_mb, + config_dir, + default_trackers, + dht_enabled, + download_dir, + download_dir_free_space, + download_queue_enabled, + download_queue_size, + encryption, + idle_seeding_limit_enabled, + idle_seeding_limit, + incomplete_dir_enabled, + incomplete_dir, + lpd_enabled, + peer_limit_global, + peer_limit_per_torrent, + peer_port_random_on_start, + peer_port, + pex_enabled, + port_forwarding_enabled, + queue_stalled_enabled, + queue_stalled_minutes, + rename_partial_files, + rpc_version_minimum, + rpc_version_semver, + rpc_version, + script_torrent_added_enabled, + script_torrent_added_filename, + script_torrent_done_enabled, + script_torrent_done_filename, + script_torrent_done_seeding_enabled, + script_torrent_done_seeding_filename, + seed_queue_enabled, + seed_queue_size, + seedRatioLimit, + seedRatioLimited, + speed_limit_down_enabled, + speed_limit_down, + speed_limit_up_enabled, + speed_limit_up, + start_added_torrents, + trash_original_torrent_files, + units, + utp_enabled, + version, + + const Self = @This(); + + pub const Fields = util.RPCFields(Self); + + pub fn str(self: Self) []const u8 { + return Fields[@enumToInt(self)]; + } + + // We have to add our own custom stringification because the default + // one will nest the enum inside the method name. + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(self.str(), options, out_stream); + } +}; + +pub const TorrentIDs = union(enum) { + single: usize, + many: []const usize, + recently_active, + + const Self = @This(); + + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + switch (self) { + .single => |v| try std.json.stringify(v, options, out_stream), + .many => |v| try std.json.stringify(v, options, out_stream), + .recently_active => try std.json.stringify("recently-active", options, out_stream), + } + } +}; + +pub const TorrentActionFields = struct { + ids: TorrentIDs, +}; + +pub const Request = struct { + method: Method, + arguments: union(Method) { + torrent_start: TorrentActionFields, + torrent_start_now: TorrentActionFields, + torrent_stop: TorrentActionFields, + torrent_verify: TorrentActionFields, + torrent_reannounce: TorrentActionFields, + + torrent_set: u8, + torrent_get: u8, + torrent_add: u8, + torrent_remove: u8, + torrent_set_location: u8, + torrent_rename_path: u8, + + session_get: struct { + fields: []const SessionGetFields, + }, + + session_set: SessionSetFields, + + const Self = @This(); + + pub fn jsonStringify(self: Self, _: std.json.StringifyOptions, out_stream: anytype) !void { + const options = std.json.StringifyOptions{ + .emit_null_optional_fields = false, + }; + + switch (self) { + .torrent_start, + .torrent_start_now, + .torrent_stop, + .torrent_verify, + .torrent_reannounce, + => |v| try std.json.stringify(v, options, out_stream), + + .session_set => |v| try std.json.stringify(v, options, out_stream), + + .session_get => |v| try std.json.stringify(v, options, out_stream), + else => unreachable, + } + } + }, +}; + +test "json request encoding" { + const test_case = struct { + name: []const u8, + expected: []const u8, + request: Request, + + fn run(self: @This()) !void { + var req = std.ArrayList(u8).init(std.testing.allocator); + defer req.deinit(); + try std.json.stringify(self.request, .{}, req.writer()); + try std.testing.expect(std.mem.eql(u8, req.items, self.expected)); + } + }; + + const test_cases = [_]test_case{ + .{ + .name = "session-get", + .request = .{ + .method = .session_get, + .arguments = .{ + .session_get = .{ + .fields = &[_]SessionGetFields{ + .version, + .utp_enabled, + }, + }, + }, + }, + .expected = + \\{"method":"session-get","arguments":{"fields":["version","utp-enabled"]}} + , + }, + + .{ + .name = "session-set", + .request = .{ + .method = .session_set, + .arguments = .{ + .session_set = .{ + .lpd_enabled = true, + .encryption = .required, + }, + }, + }, + .expected = + \\{"method":"session-set","arguments":{"encryption":"required","lpd_enabled":true}} + , + }, + + .{ + .name = "torrent-reannounce single id", + .request = .{ + .method = .torrent_reannounce, + .arguments = .{ + .torrent_reannounce = .{ + .ids = .{ .single = 1 }, + }, + }, + }, + .expected = + \\{"method":"torrent-reannounce","arguments":{"ids":1}} + , + }, + + .{ + .name = "torrent-reannounce multiple id", + .request = .{ + .method = .torrent_reannounce, + .arguments = .{ + .torrent_reannounce = .{ + .ids = .{ .many = &[_]usize{ 1, 2 } }, + }, + }, + }, + .expected = + \\{"method":"torrent-reannounce","arguments":{"ids":[1,2]}} + , + }, + + .{ + .name = "torrent-reannounce recently-active", + .request = .{ + .method = .torrent_reannounce, + .arguments = .{ + .torrent_reannounce = .{ + .ids = .recently_active, + }, + }, + }, + .expected = + \\{"method":"torrent-reannounce","arguments":{"ids":"recently-active"}} + , + }, + }; + + for (test_cases) |tc| try tc.run(); +} -- cgit v1.2.3