diff options
-rw-r--r-- | DockerStep.zig | 65 | ||||
-rw-r--r-- | XCFrameworkStep.zig | 59 | ||||
-rw-r--r-- | build.zig | 31 | ||||
-rw-r--r-- | src/main.zig | 242 | ||||
-rw-r--r-- | src/request.zig | 389 | ||||
-rw-r--r-- | src/response.zig | 93 | ||||
-rw-r--r-- | src/test_int.zig | 55 | ||||
-rw-r--r-- | src/transmission.zig | 122 | ||||
-rw-r--r-- | src/types.zig | 45 | ||||
-rw-r--r-- | src/util.zig | 188 |
10 files changed, 809 insertions, 480 deletions
diff --git a/DockerStep.zig b/DockerStep.zig new file mode 100644 index 0000000..9673c09 --- /dev/null +++ b/DockerStep.zig @@ -0,0 +1,65 @@ +//! A zig builder step that runs "docker run ..." +//! This is primarily meant to do integration tests. +const DockerStep = @This(); + +const std = @import("std"); +const Step = std.build.Step; +const RunStep = std.build.RunStep; + +pub const Options = struct { + /// Container name + name: []const u8, + + /// The name of the image to run + image: []const u8, + + /// The ports to expose + ports: []const []const u8, +}; + +step: *Step, + +pub fn create(b: *std.Build, opts: Options) *DockerStep { + const self = b.allocator.create(DockerStep) catch @panic("OOM"); + + const run_delete = remove(b, opts.name); + + const run_create = run: { + const run = RunStep.create(b, b.fmt("docker run {s}", .{opts.name})); + run.has_side_effects = true; + run.addArgs(&.{ + "docker", + "run", + "-d", + "--rm", + "--name", + opts.name, + }); + for (opts.ports) |p| run.addArgs(&.{ "-p", p }); + run.addArg(opts.image); + break :run run; + }; + + run_create.step.dependOn(run_delete.step); + + self.* = .{ + .step = &run_create.step, + }; + return self; +} + +pub fn remove(b: *std.Build, name: []const u8) *DockerStep { + const self = b.allocator.create(DockerStep) catch @panic("OOM"); + + const run_delete = run: { + const run = RunStep.create(b, b.fmt("docker rm {s}", .{name})); + run.has_side_effects = true; + run.addArgs(&.{ "docker", "rm", name, "-f" }); + break :run run; + }; + + self.* = .{ + .step = &run_delete.step, + }; + return self; +} diff --git a/XCFrameworkStep.zig b/XCFrameworkStep.zig new file mode 100644 index 0000000..36fdbeb --- /dev/null +++ b/XCFrameworkStep.zig @@ -0,0 +1,59 @@ +//! A zig builder step that runs "swift build" in the context of +//! a Swift project managed with SwiftPM. This is primarily meant to build +//! executables currently since that is what we build. +const XCFrameworkStep = @This(); + +const std = @import("std"); +const Step = std.build.Step; +const RunStep = std.build.RunStep; +const FileSource = std.build.FileSource; + +pub const Options = struct { + /// The name of the xcframework to create. + name: []const u8, + + /// The path to write the framework + out_path: []const u8, + + /// Library file (dylib, a) to package. + library: std.build.FileSource, + + /// Path to a directory with the headers. + headers: std.build.FileSource, +}; + +step: *Step, + +pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep { + const self = b.allocator.create(XCFrameworkStep) catch @panic("OOM"); + + // We have to delete the old xcframework first since we're writing + // to a static path. + const run_delete = run: { + const run = RunStep.create(b, b.fmt("xcframework delete {s}", .{opts.name})); + run.has_side_effects = true; + run.addArgs(&.{ "rm", "-rf", opts.out_path }); + break :run run; + }; + + // Then we run xcodebuild to create the framework. + const run_create = run: { + const run = RunStep.create(b, b.fmt("xcframework {s}", .{opts.name})); + run.has_side_effects = true; + run.addArgs(&.{ "xcodebuild", "-create-xcframework" }); + run.addArg("-library"); + run.addFileSourceArg(opts.library); + run.addArg("-headers"); + run.addFileSourceArg(opts.headers); + run.addArg("-output"); + run.addArg(opts.out_path); + break :run run; + }; + run_create.step.dependOn(&run_delete.step); + + self.* = .{ + .step = &run_create.step, + }; + + return self; +} @@ -1,5 +1,5 @@ const std = @import("std"); -//const DockerStep = @import("DockerStep.zig"); +const DockerStep = @import("DockerStep.zig"); // Although this function looks imperative, note that its job is to // declaratively construct a build graph that will be executed by an external @@ -30,11 +30,11 @@ pub fn build(b: *std.Build) void { // running `zig build`). b.installArtifact(lib); - //const docker_run = DockerStep.create(b, .{ - //.name = "transmission-zig", - //.image = "docker.io/linuxserver/transmission:4.0.3", - //.ports = &[_][]const u8{"9091:9091"}, - //}); + const docker_run_4_0_3 = DockerStep.create(b, .{ + .name = "transmission-zig", + .image = "docker.io/linuxserver/transmission:4.0.3", + .ports = &[_][]const u8{"9091:9091"}, + }); // Creates a step for unit testing. This only builds the test executable // but does not run it. @@ -52,11 +52,22 @@ pub fn build(b: *std.Build) void { const test_step_unit = b.step("test", "Run library unit tests"); test_step_unit.dependOn(&run_main_tests.step); - const test_step_int = b.step("test-int", "Run library integration tests"); - test_step_int.dependOn(&run_main_tests.step); - //test_step_int.dependOn(docker_run.step); + const integration_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/test_int.zig" }, + .target = target, + .optimize = optimize, + }); - //const docker_rm = DockerStep.remove(b, "transmission-zig"); + const run_integration_tests = b.addRunArtifact(integration_tests); + + const test_step_int_4_0_3 = b.step( + "test-int-4-0-3", + "Run library integration tests against Transmission 4.0.3", + ); + //test_step_int.dependOn(&run_main_tests.step); + test_step_int_4_0_3.dependOn(docker_run_4_0_3.step); + test_step_int_4_0_3.dependOn(&run_integration_tests.step); + //const docker_rm = DockerStep.remove(b, "transmission-zig"); //docker_rm.step.dependOn(test_step_int); } diff --git a/src/main.zig b/src/main.zig index a88bf8a..704d6c6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,124 +1,130 @@ const std = @import("std"); const testing = std.testing; -const transmission = @import("transmission.zig"); - -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; -const allocator = gpa.allocator(); - -export fn add(a: i32, b: i32) i32 { - const clientOptions = transmission.ClientOptions{ - .host = "192.168.0.2", - .port = 9091, - .https = false, - }; - var client = transmission.Client.init(allocator, clientOptions); - defer client.deinit(); - - { - const body = transmission.session_get_raw(&client, null) catch |err| { - std.debug.print("error: {any}\n", .{err}); - unreachable; - }; - defer allocator.free(body); - std.debug.print("body: {s}\n", .{body}); - } - - //{ - //const body = transmission.torrent_get_(&client, null) catch |err| { - //std.debug.print("error: {any}\n", .{err}); - //unreachable; - //}; - ////defer allocator.free(body); - //for (body.arguments.torrent_get.torrents.?) |t| { - //std.debug.print("name: {any}\n", .{t}); - //} - //} - - //{ - //const body = transmission.torrent_get_(&client, null) catch |err| { - //std.debug.print("error: {any}\n", .{err}); - //unreachable; - //}; - ////defer allocator.free(body); - //std.debug.print("body: {any}\n", .{body}); - //} - - //{ - //const body = transmission.session_set_raw(&client, .{ .peer_port = 51413 }) catch |err| { - //std.debug.print("error: {any}\n", .{err}); - //unreachable; - //}; - //defer allocator.free(body); - //std.debug.print("body: {s}\n", .{body}); - //} - - return a + b; -} - -export fn c_client_init(clientOptions: transmission.ClientOptions) ?*anyopaque { - var client = allocator.create(transmission.Client) catch unreachable; - client.* = transmission.Client.init(allocator, clientOptions); - return @ptrCast(*anyopaque, client); -} - -export fn c_client_deinit(client: ?*anyopaque) void { - var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast( - @alignOf(transmission.Client), - client.?, - )); - real_client.*.deinit(); - allocator.destroy(real_client); -} - -export fn c_session_get(client: ?*anyopaque, buf: [*]u8) c_int { - var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast( - @alignOf(transmission.Client), - client.?, - )); - - const body = transmission.session_get_raw(real_client, null) catch |err| { - std.debug.print("error: {any}\n", .{err}); - unreachable; - }; - defer allocator.free(body); - - std.debug.print("body: {s}\n", .{body}); - _ = buf; - return 0; -} - -export fn c_torrent_get(client: ?*anyopaque) [*:0]u8 { - var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast( - @alignOf(transmission.Client), - client.?, - )); - - const body = transmission.torrent_get_raw(real_client, null) catch |err| { - std.debug.print("error: {any}\n", .{err}); - unreachable; - }; - defer allocator.free(body); - // TODO: use the same pointer of body but gotta go fast :( - var foo = std.ArrayList(u8).init(allocator); - foo.insertSlice(0, body) catch unreachable; +pub const Request = @import("request.zig").Request; +pub const Types = @import("types.zig"); - //std.debug.print("body: {s}\n", .{body}); - return foo.toOwnedSliceSentinel(0) catch unreachable; +test { + testing.refAllDecls(@This()); } -test "c api" { - const clientOptions = transmission.ClientOptions{ - .host = "192.168.0.2", - .port = 9091, - .https = false, - }; - var foo = c_client_init(clientOptions); - _ = c_session_get(foo, undefined); - _ = c_torrent_get(foo); - c_client_deinit(foo); -} - -test "basic add functionality" { - try testing.expect(add(3, 7) == 10); -} +//var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +//const allocator = gpa.allocator(); + +//export fn add(a: i32, b: i32) i32 { +//const clientOptions = transmission.ClientOptions{ +//.host = "192.168.0.2", +//.port = 9091, +//.https = false, +//}; +//var client = transmission.Client.init(allocator, clientOptions); +//defer client.deinit(); + +//{ +//const body = transmission.session_get_raw(&client, null) catch |err| { +//std.debug.print("error: {any}\n", .{err}); +//unreachable; +//}; +//defer allocator.free(body); +//std.debug.print("body: {s}\n", .{body}); +//} + +//{ +//const body = transmission.torrent_get_(&client, null) catch |err| { +//std.debug.print("error: {any}\n", .{err}); +//unreachable; +//}; +////defer allocator.free(body); +//for (body.arguments.torrent_get.torrents.?) |t| { +//std.debug.print("name: {any}\n", .{t}); +//} +//} + +//{ +//const body = transmission.torrent_get_(&client, null) catch |err| { +//std.debug.print("error: {any}\n", .{err}); +//unreachable; +//}; +////defer allocator.free(body); +//std.debug.print("body: {any}\n", .{body}); +//} + +//{ +//const body = transmission.session_set_raw(&client, .{ .peer_port = 51413 }) catch |err| { +//std.debug.print("error: {any}\n", .{err}); +//unreachable; +//}; +//defer allocator.free(body); +//std.debug.print("body: {s}\n", .{body}); +//} + +//return a + b; +//} + +//export fn c_client_init(clientOptions: transmission.ClientOptions) ?*anyopaque { +//var client = allocator.create(transmission.Client) catch unreachable; +//client.* = transmission.Client.init(allocator, clientOptions); +//return @ptrCast(*anyopaque, client); +//} + +//export fn c_client_deinit(client: ?*anyopaque) void { +//var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast( +//@alignOf(transmission.Client), +//client.?, +//)); +//real_client.*.deinit(); +//allocator.destroy(real_client); +//} + +//export fn c_session_get(client: ?*anyopaque, buf: [*]u8) c_int { +//var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast( +//@alignOf(transmission.Client), +//client.?, +//)); + +//const body = transmission.session_get_raw(real_client, null) catch |err| { +//std.debug.print("error: {any}\n", .{err}); +//unreachable; +//}; +//defer allocator.free(body); + +//std.debug.print("body: {s}\n", .{body}); +//_ = buf; +//return 0; +//} + +//export fn c_torrent_get(client: ?*anyopaque) [*:0]u8 { +//var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast( +//@alignOf(transmission.Client), +//client.?, +//)); + +//const body = transmission.torrent_get_raw(real_client, null) catch |err| { +//std.debug.print("error: {any}\n", .{err}); +//unreachable; +//}; +//defer allocator.free(body); + +//// TODO: use the same pointer of body but gotta go fast :( +//var foo = std.ArrayList(u8).init(allocator); +//foo.insertSlice(0, body) catch unreachable; + +////std.debug.print("body: {s}\n", .{body}); +//return foo.toOwnedSliceSentinel(0) catch unreachable; +//} + +//test "c api" { +//const clientOptions = transmission.ClientOptions{ +//.host = "192.168.0.2", +//.port = 9091, +//.https = false, +//}; +//var foo = c_client_init(clientOptions); +//_ = c_session_get(foo, undefined); +//_ = c_torrent_get(foo); +//c_client_deinit(foo); +//} + +//test "basic add functionality" { +//try testing.expect(add(3, 7) == 10); +//} diff --git a/src/request.zig b/src/request.zig index 214f997..a306baf 100644 --- a/src/request.zig +++ b/src/request.zig @@ -6,64 +6,62 @@ pub const Request = @This(); pub 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-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-set", // Torrent requests: Torrent accessor // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#33-torrent-accessor-torrent-get - 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-add", // Torrent requests: Torrent removing // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#35-removing-a-torrent - torrent_remove, + @"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-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, + @"torrent-rename-path", // Session requests: Get // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#4--session-requests - session_get, + @"session-get", // Session requests: Mutator // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#411-mutators - session_set, - //session_stats, + @"session-set", - const Self = @This(); + // TODO + //@"session-stats", - pub const json_map = util.JsonMap(Self, util.replaceUnderscores); - - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try util.jsonStringify(self, options, out_stream); + pub fn jsonStringify(self: Method, options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(@tagName(self), options, out_stream); } }; pub const TorrentIDs = union(enum) { single: i32, many: []const i32, - recently_active, + @"recently-active", pub fn jsonStringify(self: TorrentIDs, 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), + .@"recently-active" => try std.json.stringify("recently-active", options, out_stream), } } }; @@ -75,6 +73,34 @@ pub const TorrentStop = TorrentStart; pub const TorrentVerify = TorrentStart; pub const TorrentReannounce = TorrentStart; +pub const TorrentAdd = struct { + + // path to download the torrent to + @"download-dir": ?[]const u8 = null, + + filename: ?[]const u8 = null, + + labels: ?[][]const u8 = null, + + // base64-encoded .torrent content + metainfo: ?[]const u8 = null, + + // if true, don't start the torrent + paused: ?bool = null, + + // TODO: + // maximum number of peers + //@"peer-limit": ?i32 = null, + //cookies string pointer to a string of one or more cookies. + //bandwidthPriority number torrent's bandwidth tr_priority_t + //files-wanted array indices of file(s) to download + //files-unwanted array indices of file(s) to not download + //priority-high array indices of high-priority file(s) + //priority-low array indices of low-priority file(s) + //priority-normal array indices of normal-priority file(s) + +}; + pub const TorrentGet = struct { pub const Fields = enum { activityDate, @@ -96,7 +122,7 @@ pub const TorrentGet = struct { errorString, eta, etaIdle, - file_count, + @"file-count", files, fileStats, group, @@ -115,7 +141,7 @@ pub const TorrentGet = struct { maxConnectedPeers, metadataPercentComplete, name, - peer_limit, + @"peer-limit", peers, peersConnected, peersFrom, @@ -127,7 +153,7 @@ pub const TorrentGet = struct { pieceCount, pieceSize, priorities, - primary_mime_type, + @"primary-mime-type", queuePosition, rateDownload, rateUpload, @@ -155,12 +181,8 @@ pub const TorrentGet = struct { webseeds, webseedsSendingToUs, - const Self = @This(); - - pub const json_map = util.JsonMap(Self, util.replaceUnderscores); - - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try util.jsonStringify(self, options, out_stream); + pub fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(@tagName(self), options, out_stream); } }; @@ -172,68 +194,64 @@ pub const TorrentGet = struct { pub const SessionGet = struct { pub const Fields = 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, + @"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, + @"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, + @"speed-limit-down-enabled", + @"speed-limit-down", + @"speed-limit-up-enabled", + @"speed-limit-up", + @"start-added-torrents", + @"trash-original-torrent-files", units, - utp_enabled, + @"utp-enabled", version, - const Self = @This(); - - pub const json_map = util.JsonMap(Self, util.replaceUnderscores); - - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try util.jsonStringify(self, options, out_stream); + pub fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(@tagName(self), options, out_stream); } }; @@ -243,92 +261,82 @@ pub const SessionGet = struct { }; pub const SessionSet = 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, + @"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 json_map = util.JsonMap(Self, util.replaceUnderscores); - - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try util.jsonStringify(self, options, out_stream); + pub fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void { + try std.json.stringify(@tagName(self), 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, + @"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 json_map = util.JsonMap(SessionSet, util.replaceUnderscores); - - pub fn jsonStringify(self: SessionSet, options: std.json.StringifyOptions, out_stream: anytype) !void { - try util.jsonStringify(self, options, out_stream); - } + @"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 Object = struct { method: Method, arguments: union(Method) { - torrent_start: TorrentStart, - torrent_start_now: TorrentStartNow, - torrent_stop: TorrentStop, - torrent_verify: TorrentVerify, - torrent_reannounce: TorrentReannounce, - - torrent_set: u8, - torrent_get: TorrentGet, - torrent_add: u8, - torrent_remove: u8, - torrent_set_location: u8, - torrent_rename_path: u8, - - session_get: SessionGet, - session_set: SessionSet, + @"torrent-start": TorrentStart, + @"torrent-start-now": TorrentStartNow, + @"torrent-stop": TorrentStop, + @"torrent-verify": TorrentVerify, + @"torrent-reannounce": TorrentReannounce, + + @"torrent-set": u8, + @"torrent-get": TorrentGet, + @"torrent-add": TorrentAdd, + @"torrent-remove": u8, + @"torrent-set-location": u8, + @"torrent-rename-path": u8, + + @"session-get": SessionGet, + @"session-set": SessionSet, const Self = @This(); @@ -338,18 +346,18 @@ pub const Object = struct { }; switch (self) { - .torrent_start, - .torrent_start_now, - .torrent_stop, - .torrent_verify, - .torrent_reannounce, + .@"torrent-start", + .@"torrent-start-now", + .@"torrent-stop", + .@"torrent-verify", + .@"torrent-reannounce", => |v| try std.json.stringify(v, options, out_stream), - .torrent_get => |v| try std.json.stringify(v, options, out_stream), + .@"torrent-get" => |v| try std.json.stringify(v, options, out_stream), + .@"torrent-add" => |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), + .@"session-set" => |v| try std.json.stringify(v, options, out_stream), + .@"session-get" => |v| try std.json.stringify(v, options, out_stream), else => unreachable, } } @@ -377,14 +385,29 @@ test "json request encoding" { const test_cases = [_]test_case{ .{ + .name = "torrent-add", + .request = .{ + .method = .@"torrent-add", + .arguments = .{ + .@"torrent-add" = .{ + .filename = "foobar", + }, + }, + }, + .expected = + \\{"method":"torrent-add","arguments":{"filename":"foobar"}} + , + }, + + .{ .name = "session-get", .request = .{ - .method = .session_get, + .method = .@"session-get", .arguments = .{ - .session_get = .{ + .@"session-get" = .{ .fields = &[_]SessionGet.Fields{ .version, - .utp_enabled, + .@"utp-enabled", }, }, }, @@ -397,10 +420,10 @@ test "json request encoding" { .{ .name = "session-set", .request = .{ - .method = .session_set, + .method = .@"session-set", .arguments = .{ - .session_set = .{ - .lpd_enabled = true, + .@"session-set" = .{ + .@"lpd-enabled" = true, .encryption = .required, }, }, @@ -413,9 +436,9 @@ test "json request encoding" { .{ .name = "torrent-reannounce single id", .request = .{ - .method = .torrent_reannounce, + .method = .@"torrent-reannounce", .arguments = .{ - .torrent_reannounce = .{ + .@"torrent-reannounce" = .{ .ids = .{ .single = 1 }, }, }, @@ -428,9 +451,9 @@ test "json request encoding" { .{ .name = "torrent-reannounce multiple id", .request = .{ - .method = .torrent_reannounce, + .method = .@"torrent-reannounce", .arguments = .{ - .torrent_reannounce = .{ + .@"torrent-reannounce" = .{ .ids = .{ .many = &[_]i32{ 1, 2 } }, }, }, @@ -443,10 +466,10 @@ test "json request encoding" { .{ .name = "torrent-reannounce recently-active", .request = .{ - .method = .torrent_reannounce, + .method = .@"torrent-reannounce", .arguments = .{ - .torrent_reannounce = .{ - .ids = .recently_active, + .@"torrent-reannounce" = .{ + .ids = .@"recently-active", }, }, }, @@ -458,14 +481,14 @@ test "json request encoding" { .{ .name = "torrent-get", .request = .{ - .method = .torrent_get, + .method = .@"torrent-get", .arguments = .{ - .torrent_get = .{ + .@"torrent-get" = .{ .fields = &[_]TorrentGet.Fields{ .id, .name, }, - .ids = .recently_active, + .ids = .@"recently-active", }, }, }, diff --git a/src/response.zig b/src/response.zig index 8964de4..a370133 100644 --- a/src/response.zig +++ b/src/response.zig @@ -4,44 +4,75 @@ const Types = @import("types.zig"); pub const Response = @This(); +pub const default_parse_options = std.json.ParseOptions{ + .ignore_unknown_fields = true, + .duplicate_field_behavior = .@"error", +}; + +allocator: std.mem.Allocator, +data: Data, + +pub const Data = union(enum) { + TorrentGet: TorrentGet, + TorrentAdd: TorrentAdd, +}; + +pub fn parseBody( + allocator: std.mem.Allocator, + method: Request.Method, + body: []const u8, + parseOptions: ?std.json.ParseOptions, +) !Response { + @setEvalBranchQuota(10000); + switch (method) { + .@"torrent-get" => { + const decoded = try std.json.parseFromSlice( + TorrentGet, + allocator, + body, + parseOptions orelse default_parse_options, + ); + return Response{ + .data = Data{ .TorrentGet = decoded }, + .allocator = allocator, + }; + }, + .@"torrent-add" => { + const decoded = try std.json.parseFromSlice( + TorrentAdd, + allocator, + body, + parseOptions orelse default_parse_options, + ); + return Response{ + .data = Data{ .TorrentAdd = decoded }, + .allocator = allocator, + }; + }, + else => unreachable, + } +} + +pub fn deinit(self: *Response) void { + std.json.parseFree(@TypeOf(self.data), self.allocator, self.*.data); +} + pub const TorrentGet = struct { pub const Arguments = struct { torrents: ?[]Types.Torrent = null, }; - result: []const u8, - arguments: Arguments, + arguments: @This().Arguments, }; -pub const Object = struct { - pub const Arguments = union(Request.Method) { - torrent_start, - torrent_start_now, - torrent_stop, - torrent_verify, - torrent_reannounce, - torrent_set, - torrent_get: TorrentGet.Arguments, - torrent_add, - torrent_remove, - torrent_set_location, - torrent_rename_path, - session_get, - session_set, +pub const TorrentAdd = struct { + pub const Arguments = struct { + @"torrent-added": ?struct { + hashString: []const u8, + id: u64, + name: []const u8, + } = null, }; - result: []const u8, - arguments: Arguments, - - pub fn init(arguments: Arguments, result: []const u8) Object { - switch (arguments) { - .torrent_get => { - return Object{ - .result = result, - .arguments = arguments, - }; - }, - else => unreachable, - } - } + arguments: @This().Arguments, }; diff --git a/src/test_int.zig b/src/test_int.zig new file mode 100644 index 0000000..331296a --- /dev/null +++ b/src/test_int.zig @@ -0,0 +1,55 @@ +const std = @import("std"); +const testing = std.testing; + +const transmission = @import("transmission.zig"); +const Request = transmission.Request; +const Response = transmission.Response; + +test "integration" { + var client = transmission.Client.init( + std.testing.allocator, + transmission.ClientOptions{ + .host = "127.0.0.1", + .port = 9091, + .https = false, + }, + ); + defer client.deinit(); + + const add_torrent_request = Request.TorrentAdd{ + .metainfo = "ZDg6YW5ub3VuY2U0MDp1ZHA6Ly90cmFja2VyLmxlZWNoZXJzLXBhcmFkaXNlLm9yZzo2OTY5MTM6YW5ub3VuY2UtbGlzdGxsNDA6dWRwOi8vdHJhY2tlci5sZWVjaGVycy1wYXJhZGlzZS5vcmc6Njk2OWVsMzQ6dWRwOi8vdHJhY2tlci5jb3BwZXJzdXJmZXIudGs6Njk2OWVsMzM6dWRwOi8vdHJhY2tlci5vcGVudHJhY2tyLm9yZzoxMzM3ZWwyMzp1ZHA6Ly9leHBsb2RpZS5vcmc6Njk2OWVsMzE6dWRwOi8vdHJhY2tlci5lbXBpcmUtanMudXM6MTMzN2VsMjY6d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXplbDMyOndzczovL3RyYWNrZXIub3BlbndlYnRvcnJlbnQuY29tZWwyNTp3c3M6Ly90cmFja2VyLmZhc3RjYXN0Lm56ZWU3OmNvbW1lbnQzNDpXZWJUb3JyZW50IDxodHRwczovL3dlYnRvcnJlbnQuaW8+MTA6Y3JlYXRlZCBieTM0OldlYlRvcnJlbnQgPGh0dHBzOi8vd2VidG9ycmVudC5pbz4xMzpjcmVhdGlvbiBkYXRlaTE0OTA5MTY2MDFlODplbmNvZGluZzU6VVRGLTg0OmluZm9kNTpmaWxlc2xkNjpsZW5ndGhpMTQwZTQ6cGF0aGwyMTpCaWcgQnVjayBCdW5ueS5lbi5zcnRlZWQ2Omxlbmd0aGkyNzYxMzQ5NDdlNDpwYXRobDE4OkJpZyBCdWNrIEJ1bm55Lm1wNGVlZDY6bGVuZ3RoaTMxMDM4MGU0OnBhdGhsMTA6cG9zdGVyLmpwZ2VlZTQ6bmFtZTE0OkJpZyBCdWNrIEJ1bm55MTI6cGllY2UgbGVuZ3RoaTI2MjE0NGU2OnBpZWNlczIxMTAwOiAgp3idb4sYYjstyh3lJ98/JyMMPf/WNhoGtrdjuusnMx0a9GVizVfwK7nrmbLHEz6WcIrHiULOm8SUYdyfrq0PwN+T+Zr5tqCcQLGxs7fA+0/dCsHp6W8Jx/qlDBM6xNkHdg9YI2Z9tb8i6mrb5kEGX44fVQJMWLPEJuTp0RA+i52Nhm1Y79wGdfve7wd3C5pQyNIqLoJ0caUU6eayEVY4JhiYM0KiWc6iqArXI5/VC7dOYDqo/zH8wZourwF0RZbGIKlrWWyfaYjxj4rXe1jbj4rPEA03fAAxhEOs/W7b4irkOfAg/K1DuaIAbU0koW/c6P1zBQOdfP1JJ4HchAAda8XPu7W66VQMoqbfF5Ve3/MBBtscTGjBJtSBLzPfjAYTm09cWPjwZkNKYHGmiNOeMMVQ0DWcSnYEPssL+ycgUW/+o3YwJvd3VFjmnYtiwXbeQO3bMRpGLA8395QRTkuhlEpdL7+IaHk90MD5tdMRpFujgjV2Vn9o94Hu8DasbbcW//UnjwV21YpSd/Csx01IHMAarWUhWXI9SLgKcm/gokmew8LLrIRtpaTPNqFQknCOaBFzsfX+d5DRKqoArc1/kPfs7F5a9HbwR09pa0hEVUX6QpXiULNjtHsLrkN5iYNbGBhcfyCq3ybYQ60sKgIDZDzeiEnbEGkIc4mi3e61Bu/s7alSlIOSow+HvCP7rhtT65wGED7EiOMthpZhorwrDQ6JczM0sfHbXxXtL4dHKDfMxqwco8veNcuUNYKMUSuOhSesITykjtsFHolqyKoJYAokMxJk6fA1+gVgHIP2f5+G9dfj7EMYuMG/TDMWDF3OvKZXkE2pYyX8RInIk4uSSgkeOdDxmrMRjUCWLA/vm6s3+9cTJVAOX/hKj9ITInhhHUG+WdI2HnSurDA4e+oSWqL3Vidp5sXTBvC6Lk0G7xG1RZWYSLc+/oL4g0x0mCJO9Xn676MRGnYNn/1zIetInEYlAr/W5aca4ZyLu+xf++RGpBCdCkaJoCYrBLAUj/jel6XqxlXivaN6MQpS1K1mSBPL1F3sFhbQ9axH8RZG7uU5EPNPchD4dsemhK0yNtpPY+jXONTxXmk6O/7zDiOLRUbbxF1S4TDLI5Vxo7K0drMylYOMGYqIGzR9eDfDcdevyTLQmXgZgwTxSM8xogbXbSo5VVrMQpJ/QGtjtBaLmtKRVaD3ZlHM0YN6iAD5ooRdCdcI8BpJAL3OcToAayRf3FZ5ZFfYWQF8rUiQpzK+slrGKp3fQMWjRIh7dUJLKZeIZEy3vyhBBTCxmeJ2y4AkMVqpnieGMlG0QNqQfmDWTxytZMew4JL4d/AS10N1VMwqKah2e+GjOCNL/EP3hsohfXXjdfXFT9QnBY1JuQtfIXP7iaomvbppEE0MRxEGczxuEZukym/t8pTr0J2WAd+bgvkRFOPGU/gOI/Hf5Z23V1TYQzUl4X2EumtrQhbUdBgidHhew6K0oe0u0nAPaudGneSgvjOG2PYHKI4Hbwv8wdW5KTIA1EF8tQNLv29QP75p2ursOobOQtCKwM+lVyK4MPU9KeLLALmSFYnlJObdofQgQ2zp+bd7Q9iFUTqZvY9lu4LqC/ghkOEzNlmMqklEBw0UN+qe0ONmHa6Cj7w+F/eRjQZBa8kOvHA6knE9WJDbXMezwN+jpdJD8YIGaRts6g+YdkqnKw6R9gM8hlfGcDDJSui4b84hxi7TNCzP5INQ95xlRduLA16xGRgBcftOoSSDlBCSJ8ya9B5+iIkULxJg9asBw4MnROkg8yPIYzSXnD539MhUhdwDvtCIbBID2iubI3TrQRr7QSFN6lAaAn1Rj0bqlnxp6oh9b50SH1hVj9qiMLUnkRjArXMUpmW+0RsE93qHC5Gba4SXK/nhfj/55jK69bf6Cfim9OUCEb7siPnKSTg+kF7G0tyM9PLNzWFtwjqx6GwWJ7rxCNu6RN8/GFCLTuBDiPi5/a5hC+lEGlchWH14Ni2/YbkzBUTQCTfqlvBAKOLEcMOxuzin1HmdQtoxztbLysPg1DBlbETsIxKt4VEG8fnWTckcY/B52cqP7yQfXKbrbHBnZRMC+dmvZbi3WmnpE9GtnD6EXRc0n0ldo+LibCF5AVKI85tv91MADhbBjvVhdi6Syy0DrRwSk2CMNqgqtalLaZRw18pqpeRbsA4CWQ77hAAG7gNDDnx99jWCfasZ5ttzCBztujOxOkgcjuBInH3Z6LnzuXek6yALMSmd6L9yfDzKwB9U63BocqdcPbQnw24HW/ILiJESvP+Xz7l+ywmcOtbEJVS5oCWLY+CUqQeT/FKKt9gzt0gck4909On/l8hfaDDXhg0STWJuo11eHWCi0zRx+dbwwkGtdZnP0hjYh0LqKAQxw9xAp58K/9jSZ708FvR1rtC6iefZ3QAlIJoVSrCjsZHczPqnrzAChDIybFCQWPPdeGZ7qOFx7agLEXwti2ltM2AP5jRMuMccqz7EtSEX2ugF8gCo9MYIri7KgD5sS5lMplEw7dYna3/EHG4KV0iLUJTd+KNjpLFjr8xNonRGYQwIRs2Derbfgbms4OL8TgfbaKVv7xNwIkF/bkeECpfsuaW3deULdeaeMeguuX/1VpxR0LLyA3b8c/CvEeVnu+tQI0C4CwvdYyXm9YI+QIeUDtH7L3aAQ1wznziSRRvoyrM43T3bRCfMMe1rNNboE6gtzH56CFEH3pupTmYH0q3cWAbihQfHv/M9Z/lkHsgAJ29gu2PULKarUQTUB/XbwoBO98f4WIS20nXO3Ni7wMDLT1NTyJfRkuftYhl/oQB37scTNnHu+j0jP1mgefbYUjYC8Ggry7+DTqeImHSULC+M/jbyjMrnFyLy7JjYPB7xV+6qFEG4SItMjcABH+Rzkt3yi47etzScNiZuZWkTEHVPB+mQpVebCnYey58vNNYc5V7NlGQ4s7EA8lcQxgEoAenms+y/JqrGAdIXyNtBhXbD+whuDYwZg6Egws+s4TPgJt1R9SqaGSOFWt35XclgF8BuOMRE2dSoOz7wX/LzGT5xaxZhAx73eK8piO/a9ZAkGfC/WPSMEcWbIbs4ZQPBgrSG+HcY56ZBRlpHg2rri+aR3zzDTWaU+e26t/Qlm9B2beS5LA3PXgJ9STqufU6lw6FDcDPr+rAp97i3IWAG/Mw+O+cLHpoX43xoH95LgCzKDtS+GFMcG73yjZ9BdHIRqLXYdI9bjvB8oGta0Xf97NQKbhnRPCTqmV9NL+2U2wm4bbCvelMdw38msPuL+49/Ej4SgrUSrzru3EZlM7uckNRAJiNliG2oZ5rHXDGlst3AxnifQwS9q6Qewu3gsRDv1Nq74b6+AvjKlUBKZ7QVRgMQO2wZs6u5kiR9jAr+8fsAu/M3l/kSdEXmfh6ZTex0GrINCRltFnAiOKRI2Ye2t6uzn55J0Bn0L5gMeIiS0RoEqi3Np1KcCN0g9FeEZP4hNBW01Vx9FSfPJRkHjZXBa1XiZ19Gk6gjVbrQMPI04TDNEQWC2mETRsIY/nBoGaC/AUfHr8R2btPV67jcvr30ICd1lyFMCq+JYsEpSeqAMFPatArhNy0cXAmw7s4mAK5OZ2iGPzUwGj5wIqJ6/CsqTuGYJq8P/pRT95V9nktZsNwbJTZpXWFt58+EmChpI/qK19/eNpfs6hEOIojMj7w/wUTAGQ745T78wSFft/ayzHHM6pptDFpW3PRrNerxHKO44BI2i26RbsCJs0qU6P0orpNpQJtTcDdPwe37hxIBTxiF6pSQB/iA/XG7+814suH8E4UmZ6U5WwcjVNtWv2X2VLx3pSmjeYlr3KgQRbfB5POSGNvzdzIS+H5PALQeIVkiIxVfPWb9EuZzngTrbLxroLnWJl8f1+G6SVkyAu+Gk2kNxZSTwqbM6Bx8GxGlEzkK88GlPjnk0TfM0wWVgT7t8uDp1q0SfipLH/KRpboYA2sOnbYpxX/8Tauwa/au1O1TkRae3GXPRxjubSQBYqx0V3Hz5BDAALMW+UtVbLJ4xQSN7XC11dPqhQF3f7lSUbzJPKbBhgK3/jflcRAs6iT4huc/WFc9qQW9MFfHBO31p25RK0PM8Na++7KBtAfr5dAZDOM4NIUqgqgwewL/MZQ7oeSarnBgnby4ZNSwGqzLlmJw7arXkgRfwdNuIXc782sqNJR8QgLeNXKS2NMhMYT6NzwVA+Mf2Gb1YTsPVmDU2Rs5Xu600dyHStG06npQd/ngTY3iyZysbgA5YDC+uU1scLHC3zwi34ixV2A4Hlc16yd/IC8Yz4Rmg+UJeYjevfPSC9nbya2zx3XKHDmvNjmqBmlcgoVvgMqR0yy4P2ceCDmGRU196+ADlvrGRoScrfWJblD4B04MxzCc227TAgs2JN6LP/8k66YmrVWGKJgPF26CHArBxTg2MrIDIkKtB6YYiG68cOfGSUx+ep/3tg9avatwwpx81SqmkIAffuQ4fnX7oE3nUdbVfYfS/fa28keIXR/yK4OAD6msGggamj9ktxNjzyRVpw5UlFKO0RIYzaTfnoEOFSBCUwIrbbblDNpI3rmjfVd6KEuTJpY/Wtg67aBRSkT508dizNoDd3GweLT9t6uLuERhGG5dMK8BMeGlKW+Qi9cjwfCy7pFAtBIKmaggdgcBO/xtLEOfqlDRSslt6VTIy1Vhs1WRaz/hmsNGgJWVlGm+xSuGPN5jymVKp9Jmsv+v7UXjTmQytxrPisBKPeJGpkB9HbqKr5TUvQWV0zhEf/1eFRE6csQIWlrLmWIJwohVDtG59pjXS13AOHiVJmpSw8yfOt4kI8GIVpWVxLsUnY0vj0+dpkvmyEDZTcTwq1GmOpUR1rXUYB1f65/Q+paGHAV1rvUMgvZddxGD35WEAJmtOjSF39zP1R8hQy5RmzttJUkYCs8OkKB+K5j1qowmOannXnv9iAjifcY+X/7n2+mfoXs5SBy7XjE5lfdKwThCuOzRWxwpjIArLwu1rT042y661ao51jEfa8Cmq8yvSs/H9mW7VvRYavSPR27CGUBq9vBndL/gtvMo2lqSL3j2hShYIRpkz25qBo5vbUS9o6ZWMpfPc+ODvBi9aPUzH+RV6Eb0GDf3eFQkGh7MB2nANZsLveqPdo+9Gf6RF+RBJanJgc5FBiJP1N+mzhogM9I5fMc2hwH6fwuV/WtTLWqXvyAW9mkg3SudrMFMtj5puNlbns9SIReuNRTpcUIR5Ui2RgnMkhqQSsTq2WqsB4XHmMUwh2MTODNDZvmS3ovsxpsYEndq9dQqD8tvIm1EUtoKUyOXeljA9kwyPi4+NJKkZvn3MoIvYhFN6Egi2nOjglqi4ohPycH+sId7lK6XjO8zfgFcmWbxr4ggxK0ZdUPkATRyn65XafTcYDEL3OL9KeZ02bsctlMSuiHWKH0klb864B5fe9NDXkB5nZq1mcC2J5m9RIjZfAxLlVBI3BZjvWtB6ALpPLpNV5VhVvBlKOpVG6yv9CK7b0hisEl4PzuZIlFn4RfirqRsEx1iTvZQhYfPeA4DOeDsmt1uqOGIIZMArqRt6IZ+Wkeg9N6j9mA5AtvuNdf8QesIu1yILptP5oY/twOzI/v3sl0rzOksPHp8LihJP2ixmwcbfUmoFfm5EkcUwgYyAbRbTwjk+UM2e267/4wfPm9oTpXpMtMyeF5jKzREZfyi53JDJEdksEuqfFx/QvKOizHdMdNEpiNPtNYAvIxkTMAfN4SHFgdt3ozTQagUzNYv0tgEGYbLksyL2x3mB8vmVD5+i88NFBokYIQvR4chxBAdd9Vex8CqOOTL8APEe9D5rGuhh5d+t1KhVbyFzGCm5rkfXQ+a31HPsgJE64XMjmwDB02vFuUBj+2doaMxezIAk3bvbfD2IJYqsR+SdusSBTgtJV/67UdNw8Cv3U3DHs6Ym0G0GwYaof8FLFOC2ilCQPu/fvKdZ9C4a0xtodc4j+Mg3rgLeujYX3sNilw4git79777TD8LGyYoyNAN/01NpvyPnxRHMUvUYehIlaZzRgQsSWhtCkqp6SXnIzk9Un9/L8MVsbf47NLbEwYAaqk3odR6At90+IzyItdkwx8pSSrvxxuR3tdxB44D9ZLEmcNg1gC22hIbZ03UhypIMVrHiCmmdUWYYrJue48zHYhPbHbgzPee90uIaPcoDNw/pCvNtRHzBDK2LQkOUPBns9zroKA7PSWU8TEyTzbEsEWlwEW89A9GTFWcJZrt6zls8bj8VviXFqDXd88L/KmxX9VAHpUb70HRskgEUOkLH3EBlz683IH7JsA4P/MK7OKngY0SJWd6Srw6MJV24N9yqa31zIUeS9yCRsr6d3FL3ta4oAD4EYIkqWP2VnsXQ+HvizRvRmR4FYC4Bdn+Y0VewPON7lV2j9yu9VggH06Cz3kHtSFe9IjLZP2NnR7Ykpcl644seQDsP0yAoN6pBZj35YGE1RWloJi0gvWzABA+8L4CQks2EOoagWo6vNDkhNoTsm+gSxnQpHYXOg5rIB/SSH+Xen4gPK4W3r30GcXfA50PGThatYPNsaW7oPBvIszISQa9dSeJEgB8m/pdKSyT/nrymbVzKUZzpDk0ICfRJLM/N0vRFqSbn6jL2ORosmDtKTzef+6ySnHEE0iNhQpPKF41RPtwZMaxaC+C/PPt+VgwE6it/9CycL7qgRm+1g/2jHfBD0Hket/00FPZzrOrjXqm1PAxPmcN/J9ESLjZymZDORWcp3PM7pAr6CTNrVrXzgagOcNZpNQayM3gMHRnj5h21NjXlzLlqkpWEcxlb0wNDWBrWrctBUk1IM0uGrfS3ewnauQ/JPpUDuJ13Nm9kQCbW4ZM9bpBwbr5HNI7qUlWuZ5a7dKCHgjRMo5sFzu74xGSWtQM5uaOR+TD6XA00vf4d6gDBSTz2x4cIlI2KMP0DMUWAX15zMfawDRGPVkPGlx4GuTE+WaMXgqc2coJn8bIt6FPYwgx6Q/MeaKjoDTBabgXfg1kwkdGHfbBQ6R0nuDF2WBjsm1VbpqgNQPR4maT3xEZhJ7NbmdD3WTQs0+xYtkZgHE5yfmvwq2i9ubnNxzU2ysLyx/WjvfBalIwv4xmil/IdsftsE2RB5leqqhHz2HhSxFgmh+nd9vZgrpcOyzuvXQ2ifJsny6mAFc5VnvlcaYdnXq3UQB9jXlVa/QualXWxske8BO+PR0N1csibnZ5Gt/wlv3kwb381Evn9Frvvs8VnThuh6j5I6/9m4pi0yv4/vAgCqhVa0uWcVnYTvvltKBNaLVeEnLgvTniw928cyEXzFNMMG7N9qWV5XZhQO96RHrMw4T8T6t9RRgWIkxrzDQRqjW6OLGT6IqI/farPfU0l6L042lgkn5Edtr9eScVsQAgV3X5G790WI4nf7QdEAlD7OTotG7h0XNYoJ1l1ldUs+XJ5quWnvnj3wpsDo+m+0jqZRu2uS/ml6AwPH8r3SmhVwhxpxV9dBNMmh+vdkP5dYvmyGvCX7eWE9FE0poTHJtpcIxBQI0n3hWcM95r9coYVMnyg4p5D/QWTsUzSCmhzxOHbBbZY/rDLX/ti6x2IQhIvWZinBAPtbe0D6ErF4SjcXiVeJO3Cha6EzTDVkxY7ZjJN1A+6zSqS1ANQzT0VF6CHphUVs7mBFFxUZL+NtnX80vmjDoGy9VPU0rfVk+lwDJJG2WfP8YAEPRBmV1xBylUM0S2Ft4QnRhtE7EwckzOhfUIioEjZ+fMM5c3DGQhPBt74a6hIrpEVIiDZ+8luf4KSELZTmnR8+D0dUi36+A8n2UcJeMO9nVfUvQLDSIouywyCI6h/dwaw3cDLQ/U6ryGCglBzYu8Nvizh7v4jMemOWcU/rk+afFKZgvEi+yKkr55ZbRYvH3V7eO2pruiy+wp8rVoYEnCwmbYbcS9xsbGtxf893k0ImoT7NeuHWwbOUC9Cc8qx1pX2LrLHO93xHoGN1WEz8MSGgOgNqhbSoW5adLJy+LqDW0sEFlCzZ/yQ7a7E4XP7hUQrXxW41B0Ah9oXV/MjRDrJQc9Sm86TzirXqnh2huhtEDrm1BRgHufqmGVfVCxQGNXqd0tO1YFfVt43O7dYWs5uiIJKF3K7BKf6fHXlZobgKpz+ueLPDnaNJoGN8CtJpXy1iJVco+akgVQMxUGU8rAiH6awXLBkiYQWQwLp4Y+HWCu6MiVxwJ/6H9LdOjrtWsFSlQBmR0LmkiibxYg/7Wv9UEKgcChxTlpaz4Om4ZtLiaBEGHq2kYOIOFvCQC1kG8P7aNE5aXCTqMtOfoMEa8P2F9Jadhtb9T/yLItN0HbOEDv0OeROdkGu5XFklouCi6kSnGgUE7HZrZBo0Mrri9POtJ3qjcap5IXmqcEhOG+xZz6fxmn3qpEFhAFep28DE2OGLsh2pX9ER0DTkGFbERfho7eNKdwJY5bvYlLKQaAYRVcqfrVvqo5lrGjd5DKzyMPWHlak+EZk6P1tPv5MjC9XOGOZYqcYjD9bcTLKQmNk2cqCwi8LEuLFYOYYbM95fuXpV5/p9EuL1Unb5DFYkulaUcThkDLcYnS/2MexB7aWU8oqdshXmS8N01nlhz9G7zvfUBJ8u7h8qtUHXEZ83ZPqRXOvLUupv+G2jZnL9YPmYjDMPgE6YjaDwwP7YK33c8weLaiXVRM0f46sgvVqEBQ3ywOirXfzGgWl94R5M7+kLXpe0M9Mc1HREZdNu2GsxXOsaO85VB3i1r/9jpXz5802hvHzOb49J2kkU5TntkY8bZtPggq4shDm8NXN7QN1hvNwxVoUZQECYSGhhSyK6omlCTV/CvN/5GE4OPooey5Iw9gPboKn8Mx21rGQxGlQdRyH1eIHJtwPz6wM06ieBAmpt2FYnzwpD8iuvsFHHqIFDpOtFs7Hhk/KnlTx3Ta1eqbFYHHl1XV25tqbiGQviPBLx5SfITcF/1KhwsUO7j3wnFrZFAciV+2q69To3ZS71SyAruAxmkuscU1hs+5aJRt0cOHchraLTi4u9j8xE/dExfWQp54LsfaSXODZsxms7HoF228p/+1NYclO7CT8iFxseLKqti6x5TyMN3h/21jwGh01BvCFhXgFthFK3idfi9R1SIcpvVbyx6+GhnV8EWj7j3NFiYqpskgrkjaem9c143gbKiHbD/H8uvk2gtnnfjEOXdd2YmOTlKj/KdE3AIdEFT9TqGTpZ7P5zAXxm4H2BzX6BqwzHXqoFxaMhjfhJ2uR6IDityHtm0zVkwULxzin3hJ1E4i9KvunJ0w2WiMuvjU9B9p56pElOtqsBzZKWfpDJdxpaUH7fn2gZCbrvRFfpBrWzDfLlI2Frwt5z49Nts9xR6monBts2W8hEPRlyBK1sdyyEo1juT1IUtW1nJWteSvbHHCgkOpbCednFrnvpJy+sVcKj11/yCEcJK3tK1OnvvsAbWFrf3lfOEi1uv2mZ3esZCdblHp0QKjeuN3Kr3VRzeMdkEJnV1ZJrnbiligZra8UX8KW+yrLGuJreBkksc+9yafOOuQcXrkbd96/5O5xWWVc1sbB9GkOPXRhZmb7JWkJ2n8czG1EkxVrkxfEaknfYdgIgAPIVzvAX6nwFC7/tBh/BdUXAR1DwnqnLDyCapKJZS2CsudMMjQpPqfglyDhO33W6NxR2VQ749JNgel/sQWGvfuxq4rkUVgC7Vbg8FOvdV8505HmKoEqnACN0zkrpYbRdmzJuP/hqoqNeoAPWiENlNVVAyh/jMpubHv/5gLshzrYrdj/Pmw01PwQQ0AvWxZBIJhot5XY5kT/s0QvkYVoYTCkCiAnXjYDwAzy1ElLT9qfDYA54uWX+GoN4wowu0tqQrh/NWtdVyUIIu54M0CLtEeO1BfAxk8W8Ru4OE6wY/qMTOWfJPAgNfywvj0GCUsQMKSbZeP2AKaEDxrItxKpqOmRqCqJJIvktHDGhbV0yJNrN+2Be9SrzfOeFzns58qJn4NH33qNF1Zo7abgJAcInUkTvG0h8HtTqINw50fRK0jS9ztVXMLoDKoeoTBzsPVHlcHnkeSCSBc1MzazecY93yejrLMPRwzJYaTO7kSpPRBh/lhsCqxtMpzgfehHVZ0APHpaKfYqkZ076f5mfU1NFeiyelq1qyGUuRbdv5rxnr9xzwHj1/D9ciJuZJxhYR0OHB+iqQKS1hVX87LEvaiH2muOKEl3J4XsuO4/Kq/AXlpIn947fNUaWy/mvrahPl5bwgZVvp5B9fEDxZih1KWUGjOPWHmWiUCVqIwWKapulcmvSjeVDhIAFbcUbwTgzRpYIOcYNzKCxylnr9of1+5Vb7Jo4pRORS0s4m2bCReIXedLpxioLgS4LeHGsvN2B+4z0S9WwGkvBs0jJVpkVCAIBf7GakUGd55wsOIEt27t0aY5lqxN2vmUL2czj/Y40NbHXf1usaHYsV3E5WIhQ6hcJidgiMSgk4RT7adcl0IbhcHvbstP9nxRCpcBxHijR4SboakCpnrUtRqE/t2iWssyR6WK/Q/67Xt5sx0S/EzIpoSPFrGAJr7eR4VP49N+H1l+ly38bTbf8fgL+pPXUvy2OdMMxa8jC/xQdNUf+ADUo5lfg4AWJqqARKbV0zjLZ2DqvyrFUbyzYaRSIUWLFiLG9n0DTl4DbqTw3hWmIlZZAcJpC+3QVFEkR08+EsnuorGJ8WwxgZE2xZ9DKwAb7+kJ6ePEG/TdOiQBdthh6mcmW9zJIWA/Nm68oPPXwmtzwq0Az6mQU81+TjKGBhJEIQOl5sjQX7nhUSBMjKvl37Njm0oWbCizzZZZ95VoHVadQHGrCHi6Dl5SUxO/GTd+jiMsbW0lZi8ooSleeYEhbHttlM2stjlec6Jjm05jboFfpe8K4DLldgARdd1fmFU+JRKMGdZ4K5XFKZDBsZhjECwdCFfP0ySodQakCdA2wQb0Jz3d4YRlZH0UGyCYqXt1lV/v5Y61h+wWIGogkotg+jy9TA/i3AniGjWKqSw28NXzFdpFJ2fD5dyiSMAD+eiCrM2Pm4+zDzRB8uSwfzj6q/lWcmtr65p+JyiXFXZdr9sSfr9Ida02oysf9HVKqReIttmSRrOk+yvple/fOrGIpSNFeJteUsKLiufPEcqT2bwqzN3wyu5mVxNUTqFROKnU5ZO1Hkt2QeYhhx8zJV89R+hqiOcDVcjBU0+gPMhHcRL5lx+TsCy+m8AUn6UD3toikMvzUMzZaTR4UNfpx1bN358nUCJgaNIbwFNTa9gNpmqop4rQwYxT3hIWBB0tgOcNt7INP4P6SHtp2OG47xLZ39i2acssF/VwKF3H8LpLlSwg5YZGXy2Akr26M48KkGULdjwiSbboglv2L2g6zdE7Eny7qDgZTPDNjZ5oXcx5afpy0FXIADT8pdj2V9j1lzrI1H89QHEFuO85trmMtI/HXP/aMIRaKLCb/rXhuCdr1m/OOhIX+jrvOsoZlq2TJHcCFMgxK0H4fMhaU8dvS9JYZYxutlCDFWZLiNAwTg6cXtfbqZU21llcDyvfvctORobxDoyYqS9hxa3XpNcDdGGjsfN/6GVW1+78crsQQ4n2frk9vJltD2G3ht8KCV4tftCy3CKHGQ3rDoc8gwUofUokJC0cv9Wvn4Flq1NbftsUPHl9fRQO14HWG/TJVN3+qTj90yWn/Jqk7vU4GQYE2Cy4XgkFnJVxrhVl7z2Pt01Ke0gwmIbi8bvpzLaWzqtUk2ZqA8nKARyW9w9yLYOikjERfJvgQSZFCieBdS06krkT17UXjyHQ0levDM72OHKRz1MHV+5nzsxDv3Op/m+RCMXjndMLnr9KGQCnuhrgjWr7MLcLj3i8qokj+nncmaJANgLouaP6v3EHM2FUyblkHZqfmAR2shEFWsujRsb1Sslhc+Lugq4zLLXbAsTQEyQKxvzYVvXI5TBIcxgJiwcSnFN4ihQ5bqjr01PSeAt1YrQ/YYY5cZn9DtDDXRbuYqexalfY7B47b4CK6JrDPKl2p+xx7BE4mxqPkVDbwhXblpsCeIirf6TwbvSfi4XNJg2pPscXcsMNzVrWT5jUAHdgmer9CprmgoDMxVJqxQpi6zFu6lYXkv9B4HOqtttRbqvAzSJ3SnJUOGUxkOGZ/WuVojroNbIxL1O24Jf8MKk+979hAGYMB/v0qu89tmim8XY5iQ1vld1cyi8Teg1u+/PR40pqT3F9MszuamtJLoO3+mVOeehKDd5vZuQGC4eJe+d46ZPYrFwjQvPQxm4WW3F4+koUKgeRWoMSue9d5YGL/yK1V7QvgkPCr87NEWj1JZO+EiACJLyKYOZTMAliKuyssKb3uUisW6wimOCXsy6MCzRA/Mohcg9nWiDsCiIAoA3PSvpINkM3JDt1MuyKFqweUCdRBhm/tJ0aG+OUDP18nVLM6vkXHRyCC/xDGv87kpdcoUDDZAt3rQqW+x/KE7j+HLbHQT+vt5Ndvavhu/wsTGqEYtaWaTFSNHRIbpL2ds9T4O+trPaI1iQ3O+ieNV//X24LdMl98M55rFrxlsfQG4QsBPoQfi144kUUb19aGcwS7xqo2pe/1nrapbkfknlxqM4Qr9mw7q6in67oDA4tkPs1FvdrTKduBrzp0lN0WVUtcww6F4BqyW2fkqW5frk5J0Hc8PG5iRgWSdFS3vVK42kWTTz/qGRwcKbBXnA+GF10ynhu47ICAwkhBD7L1ijyjAqF6QPASc/0DTx7uUbA9fIqk7h0zJWRGdrwYN7g6/LZeBr0S5Ai+vhuey0+8oIY1bBEHorLoHzw9aIuCOpR+fBK18pzadCnv0XMiyblSetNcogcZi35tao6A6KrhqYw/R1dkC4aG/baYe41yENqWqGGif9rYaclJkDfNKqIs0X4tcdwcXXUmYbiICCc3n9MPbo/niv1/XF01Fw2rFG+KatYbg9x2XSkJK+Ww+2uUa9f7rC/VnO+QW7LrwjM5MU2kNiBmsr+RBFt8x1UK9yqnTuL0hG8bH/tyKwiIz+gdgBagq+Sn2D0SaZ/WfS3QhvIpx+9CAYOODsMQMmbmfeUw3Km6kckGowvIjFNq0Jq4ZNTDpAUTGVNzg1YzUGPxUBT7vYp15uV+D4AZR+tFWVz0EjZgW6yNHGSMyMYXYxHlgNryr8c20/aQMQeX17YL5lUlMOxGzpo4I4HDt9LieAXhLTZruujQsqaCVMSAUzqlC8zu8KFUR+WAbLFDXm3UGg5L/A26664qN2gu/ewANwTBHudj0bkRKAXSSAgWyZVFu1LJAY5QrzCaKPvo16kZl9sd/z/IIqOHolmOLJPpwWPR5p0NNir97NVXJLCY3CttNm1aM8nc4ohRzyUAnNijf0sl5sV0HdJ8DVjjjXG0ZBGV61GVmRfrL/RVItz6b8n8nvCzlk6ZyR/9bUhkpgqtTXDyaYEo81xmmSgyYAkGbg2BkKDQDnykrMRDNip7zgQd/3emOAdiBIJhdfHnfILlZDIXwggpC35wwcgov6PxBcPecEzp7GUdTTL/WgKnMhMfJWioWcsdTK6K3RBNj/qYTHkOyfi3RJgzpsUoVMe0u39s739ztCjjUflSMWfZ9eLzU2bActbSb6z02jRrHBhav/g6BFyz/4xX5z/vIVrousCGwbQFMe/r28X0Y33xx6hFjLUHnnrwomLiSLh1rTrwL3oyn4k09Xp8Najs5pFDSK+YRgrOKbWO5maIZuFRNba8c0MNZk8R309khMkzNZL8mFMB7gJERUbfw8/bh8NgBH9DwJ5GnOsuHULh4n9u5XVWBG3SE5qLt9uC4QeYSaQQDDSV6Xw5zWVtA/4CQmrEkJRN6BrjzylkIFXJh2AyOGcICp4LzWRD9S9B2GTk2iGfAMoyxlDFHRWtBMt5PuKT6bAXHfNl4H9fBy3T/1wancaBpYVbVAaTM4j6N238B4a75ZmnqrFmoD48yNpP42bpQ7wetr/USuMfzGiCePivj0WmeijV1/pbB1+zKSHO/kmB8FdY91FizMLkBRPmvOpGIgaS9fShEIWTGvCrKz+QCInRCu0GzgDqJYMa1Tnc0MWIoJTCNZqhu4c5RmM0rDdVymkmQJnf8X65b9NzIRtcpFXny0eqWzu/CusBgqvBbhIGF2DlM1ya35lJ1VnDrp5W80nebXigSrUy8ztNMFpVpOkAr1sGywxuWj3ANTCjd1JIl/md3Q9qy+DFFE9BWAkATMWncejv3HvBo436mUxwVLrnM2AWzSBPW513CO8pv9nJwPtQz4FlMo3HSrlJIU6GivHWWGHydZZisWRStWQwIAjCcbECSyeh3vzQFgSiB5Swie11o6UVCNAw2PStplvWsi1H7QoUVWwCp5B7WvrWRtw08P7k9HH+RRgR1ecHoKiwucYGKK6UEsLq0RNckbVV/zYKXRoeX8e9rCL44f1VaDqo9GvEpJY9UEpcYa3xQNC0yKj6qvDPS7w+bCZvyOecMvdgY60nth8Qv3ET2uTRKgef9IFD6X64B9157n2WrIyTpwp5Rt23NqyfylEjg6Ov+LJE3OAGyS0ud0dQAin/YxpzgXAvFRizsOcD+mujwIixiRLeQuWCGp1lDd6gCEPNPvszUccoxDMRxJuoRqiHO4qVPmFJhdz+nfY6YlJUjW9IAnUh8P7MvE4W33YP4nKck7L89FuZJgePfnNVIVvf80TBXThAEhpqBNJaZsexVggtHl/A8N908OAbDJWSpyWtUF0bBbPn86rKtLElEXnNbSkz6h558fm54uIOTznkVYdsNWX2NqoDt36Il7GeiCr4Fc4VkZvsCWh5MtcAb6t3DigQCfkYXJyP/ntvcRlc9fJjWJTr1hVAYI6AKud0hpQ+FhDlSLGYOFH4RGk2W295P3olKaG/qvCy0UZIip7d2uJ9/fnUFg9HhZh4s2G1uXRG5vjvzeaBeICn7VW90yusLApp8G2IXelJabQtxYAIcSxgmHHNWoxtmGraZkq275absSRxzAnP7i3VK/tNjUkjBy1pi2iMWRx5iC3uu0mn2Fd3D1v16j/BqNw6a6sLQU1X4Op5VwN8KMljJ7phgbyZZyGG69oSF9dXBFclVGheUx8RecZMfHxdG8xQkVv8+f0xXBBXc/S1AGHHePTSI+VeaiyXRtPiVk6EffmtUEe6Bg9tYanX5ij1rZaXc3YG5YR+BgCYtNhmsFVyME1JKVQUMdTxc4P1x4esrJGfAbEcqHtrsHLcgmsbwvpQVSTDbb9w8iCU0HKV+GjNgX+3cDFye1BvJEAz7TBluYoNwWN5OJtPPbTIy5oy3C2OpLOnyMCa9WUBr388v1q8dXPXy1V/l3C3Uywfj0VVS6KHiyrtckW06slqn6VW8GhJEpVJX+tQatE9PrDcHm17puDp7hTCJuIJ7i73N7iFonUl28FnWpQey/ZQ3Pq3JEXK2jfQ+QN1KenwkrEIs+sKNEIk1eMIReuDOt3Tvwpm5+g3Szy1b5v3RAjgF9hyBHhyvfCsXqcOoBPZF4RvXddqK3GRUBTVJ/NPslMy6Lh29zq0bFKRgTUmAbrCqeOiTOo+zdJfmoWAqc9GVWOJQSl1HJSFvZ4A4joph8RB1WJd644ZHVutRzFvRJULumpovRzr6HPMJ+tMQccFwpyHDOzRNllupdF9sNsnp3pddsfSJhB8nfWZtbylbJDL8WIkgMdaD5HiR/4ik71EYRb1fWzLpCmlpRZ/wVwBvua2rv2UKJNEMNwUh1LrX0zOQ7p3/5uCFv5yA0l8hKcgHrypTphkKZubkdyirBcnlzvoIz1FvUOmFrLiv4udMaX3Gb/ltzXrgffPctshwM+m6tEtNRhGf7jhT0f4q0nIsyxGa0ruq2VHMlYojtdrWZ1RpPtMP2VEkf6/ADwklNFxJIdsr3BoISCY2CpLq1X2tC81xOzZ7HnemlttHvVH/rqaagdyGd3tromi6BQc9vo+2PMzfTKCyq1T2ST10VQB2Tmk/Fyla29dEueWH0aSS6hYz6orvFctjf68GUQmOLRcdifHbPFChiyV1wrq556xtXpXPmIRBYBJv9a/nu5ZNhgD6wugBD2BTLKr5So4xyw5uHQdB+uxY+MzR9r0eI+Vjs3iLHG4J5HzUT4doE9ecnUfDU/jbeWwI/2jnRS99MZ/g3UjHTIizcLIi9tbDd3+flWWZJdb2M4FzngAn0bNVElV72+ZE2H1CDlSWRLjyTX7UxMFGEOTx15zf1NVanzB8F/BGC2vlmfWOkZ9SNQ9kTsEAtZBUSdDw9YT3UEPquwSRvyP8hJfCe1NCInEmMAFQTdO83Wrdkz8yEZtpAqf5FJOeRrJLvn/cIXB48x2Ps0xic/z4vyD8APbO7noyMIerZcfgOqKrx3g4YHmKzKNLU/OzIEsfYpaLzYhFaPVc9J/x3617sEebxLD03iH7v5EqZAgHhM4wI4TdA69a1nl58gGy/clfhMZSVKQlbV+aBDYOxH8BFrIjccKeObT4ALWmhkPtRbghXzhF3g4Tle8WtCY6Icuf2Bf/nymJP01zt/lVAH+OoH5otfIvTo6Ae5ygGuq17GbGaMZfC4gq5mYJr8GZ9aw6UjD+QiPpu+yTU7vTUrhzTAIFmRAcc7LUKfMRrCtRWZqr73CO5zknmdLJqzufc0QNT/gRpmGRaFWg0hRu4HR7h1fGrW6KqpyCsN1MI4URVPvuY6s4gkKPph8casTpvTxIEGG4i3BYKVVG+48PNxL/QNaZpnAX3bDTUPUl20NkDTD/itUf6xGEsyRhb1sawVTQp4rdqraM7IGVGcT6131hdG/3VdUH56fMudlrVB9U43BG3qsevPTvsPyLwcXINyMyEOEMJarERVW61QFvqIxC8WX4ZijTEOkWnvsG6aSTgDDNF0MtDJAw3EbHsh4sDW6V8cZFYDX6q2NP3ywVlzuElM4nW6mXuYzFigMu6WYzP1cxun0ro1etrpHxMxeil8j89yMDalXwz9vHfc07C7K+veOaNUP0hQp4xfvaml1+2y/geZLW0DqRSk51lu/BceRxUy3dgtoQStg1tx60MbY5fU8OmC8f68fIqgvlvNLrsf4HskFfWuXdqdh6MhcI5lcaZoAMDz0a7qsKTEGdE62VxUGvAhNHaPIL4OjkWfY0XnyHZGxYquIXwbLcohkaqSA9frfFyZjhHPtAC6/gObt1FHN3lxtk1QU5wmTIYpawRZb6YHdwDQINJ05SnstOjQ/buRAYENBLlnr5/fLsZwYPypPf9A1w/WNFoy1Qn0AwGcywxGEcYD5Wqed5uARuWf3V7jD5VFe+NMsuTheOm8PuwZ633cm4T6tr0ZVDuaLILUmtN9kmTrDcZ2dZnRpByvWWXTLWals2OJ4PPf+loDgSYWBu+tj35K+NsskxekONcYsNe7B/oCPtEQS2BWXmkxd72dkpbCU2N3IQrleIx7XnpR4kUsYFwyvfvTQJByRRwas0S/jdZmxt8B4tKaSnr0BZefnRDI1jeC1re6baT/MrqJzY903EMSwX+J2RKQrDHuZW1Ps2HySLV0xbnOZD4vTX0aJHzPbFEtoelUo66li1kCXK4NJfUJQ/LgWZAU7CSaxAmrp5Ay0pI1y2xFMlYslE+O0BkGUGWKNx4i0QO19Q5l7o0dg7qjquAxgpo4zgC4WnyAKY5YJMU3/M86LGjdPLPeZ/XlwiL6xOdNIurwUAFbz9YfrGZeZyBoa2jZp5AQAL23xrbV8qg8O5P4cDrpp9+UDsvyOkQs3BGYZ2rQY9sm8JQYIr3JjLHfHH9vC0XQ3cBp5PZ1ICeqw3kaulch2GAnBmiEnhDhRJckxtvIcDY9+NS2cMfesaAIQYJzu/Chr88TQIUrgMga6UetOc8kEYy+eLCGM64PNBDHryUwFaLsfKyakUR8mPyY9LMmdY1FM7CG5ENwniMe98oza2+oxXGu0KCeRbTExd4hWMLsXEivvHG+/umMM3L5eDl9Bt8VDV2V1e3AkONS5nvXqwOAf8zsfOzxLUcVqiotMxWL5pQUnU0XjJQYvlq5kCfRXhWltLRUBUO/oVs57EioR4phqmcP8wG54P1TpHuLY77IcD3UxgMadxmOPhSR3OP/4ekGFYq3SlwrvbmUAQpDrPG2izazJIoHGoDvrk9YTPMXX1izAtycJeRK2SkVEQun8/PDrbaPJ74uFyFBozQ/Rjm6jjQi5b0u3A2yxhtYyrNPhCxEDSiYkGF/O86+hnOuXQm3PWST+KYpEs94FobpfLfzEJIGCNQTIHkpYlc+/fqbmLIHYb6/kauE/a8EfNCyB/8WD00Hy9LJYzKaf5QYVwkyoPmL/Z493SaDrslU0FedpgatHRprSmI3uxITMCLXVzfU6DmhKwVI+rqRLHRo5xm+BTVXgDG4dTjIVgKiVW6LFSn4dJwXgKbTQcZN9k2JCfXgJWSvMsmTnfZA6EXH1eqELdwCcU4E80iFN8rNk228lkx6XaQv/YN5zMTcSK12aNXprU+HHnqn99N4n0sZkhX3aX1AaEuKEqC/RG0mgEVSOtndc/Etd74MuxpzMVefv9anZLmwWzuMuWHeR87G+9X1IyD0Wvk/hLCwqt7jYxfS5fVBVflTbvnfjdt3m77hkVsRhAmD6yxKWhq4whhIuaRRsLBolhefoWzU1XSgy5/jvtB3ja9B6YbUmPLQ/ogfPr34ABBPyVvXau1ZuTxXVrA7eqJ/a53uELQrCFbI5/1OWe8eoPCIJSwLD2hmztrCk1cDzLtMh8IHALRtHEqFQzT4Qdin8K9Ku6m1FxWym9FQzisOb0XoY/7NwQGtgAEgvD7snty0cfq28l246ZsurJOLSYiRGhH/Jjw0K6RL3les4V3DXJniEAi8xvpKvg4njaS1JcO6m4Cvg4k4dFK8VzLIFuUsPaaYLL39RvaPqP49wGds750Rf3Y1/g0GXuYMATC46ktbuLI7gvOpmZTLgAlrSCtv2C0u3kg8XoYzc5w3xVW7wYKJnjayqiNWhUw9TlEPAlTn3A3Vc6SzIvmx/IXxGMOBDtw1tpVHonNJjj/rxStF2/GrHWR6+gCx3bemL0HNG/ZdJRIFhvsPQC9O0FZl7zE2/iGe/ZZ0novFKerSKgMVbEM88mXff0yM9G5MKSuB5nBz0cPLTgz+mVGnHLyRp0bjNe8/fUlzlCpK99s9RNdTcMZBr3G51LpFYr/kkFQE2mWJPAHwZ+om1DnXU/L7AF+N+QgXoez3MCMNeFFBGg8KM2eCzaBtE1igS2Fq7QWVDunwuDBLmQ0S0XBOpYDsciDR2AU/1pt0j1Q6Oa60PYpa00guZmHvLK/i5kKj3w213pk2gAOmO3UJfauGEdgijWIK2FWXcLg8/3QGnhKFWN5pn6eo7TXDrEP9pzsBgNSGFgVdamY97D4lOXGiBjmTXM1T7clazModbXr25291KtlWFJZYQtKKD2TDugiDbreBp7IjUPkllQhpVdwsAo4f0ZPzkybXe4YbsCNsk3wy/Tw/iDwi7mreq4B4Rv68zdcRRMWD5bWlKMKlEbHHtc3lJ4PUytOrcJ9Zll2xif4uq+EpyFkQHYfqoRS3lvflf6CrJDKEgOBGsEDvZAgV/2gjb4no3++xaJzCW98v+GVrtW+O3zxzwY9eqouf1ATr0eKapZEsjROQ1S0MVAg/pH6+oeChToe3qNIQxexFNJ6YVjRNd13MlNA7MMdONnFmaM+ydpb9vX/D41YL0Jg4W4+1pYQZgCmtXr4wVYdcPw7vGjdWa5AHhHvO1S/ramWKOTjomisoqy1X9CC1Hlr/e6sGmgv7POzjpSxKvWzByOmkNi+K2qvRl9zmeyyFVf8zVoZLZcXK6CNBDnqfvoH64juNljgaACLpz0nWBD+jyrLG7frhTChMNHjMOqg2OsBGXD5Kl15qEGUPfD2mcQ8LSCAgGe8cGMudL0owRYmeJSJZ9afQon/Gwlh6g3Mxb8V0lvrUpHINJaE605BJrEUeVAE+diSp8dLpUs8Bz0tRBTWiF34dROJJov/UBTWPW+PxUoCc+EwycUtX/AKMTSXTJfXJLelibHGlbPq8RVHO/Tp4GtnZcaSQYZOOPSJvb8yuhoDrxZ3mfmZ1gqFIau8Hmf5HfPemydYD7pcu+iNsPM2uugAhabd+i79P7h26kNYmZAUh114kqkhYh9aq5++hdQVS6mJTNoMZ6hGGXCUgZkkZMqBQsktYad3Vf0ZnCAhK9x1HC/VZtl0RfpOfE/qSeBJFYdbSHjj+udjhsfFRTOUpmVV7Ibz6DXWc3ys03GI5njFz8oB2UpFuqhOKjRzWBZuCDoA9BAzqUf1nE7eK7aBz+Cu83wBqDVpN7gvhbY2MLGuDH9bhfpNqyWNINJFiAa1RgpLylaj+yy61SgXM/JYLjFq8r2elvDdGor3KpPpkymKPTm4YhU7pYH27J2fj95fVM08sycyIvIn+GB0KqVwHrjAvRYsrI5TrQdvtuVju9QlKvyAe7alcvSySstBhr5FaH9oGOvlcbVwGk4gZuP+8JEVqlTmZ817bbeyXH0beloMN9cGfi8zbfe9eoyhL2/HL8ooCKEfbQ9dfRywf+rbBRBk5gQl+Bqlo5Mb9OpkI6dAiyQcSYTHf/i+kYScBAzUYIFEOZZk0QgSsmInuMR56uSCE65pUnG1d+ajCUfPJgR+q81AM7ggSY/PsWtqoL9A6Cmkx8xV3a/tQczFKZmm1dtYWND2+kitkb0M4uZ0e0mLQAOiMz4dg3pHTwSSgz7KJSdO67/DzuEQ/3+nRGV14rFz5hVS9vvRRIGZbAjW2WqTUGzab1SM6+pg8LEH1SAeEYzgSC/x1VObXFmUVJIXlSMKVS03i+4T2ZWXNUJLpVh9V72gkjpavesUsU+EEUSP7tdwsmeyJDjwOyLwGn8QkaSqF44J8V00b8rieQ/y+GvQpSOoygAhenhMR4S2lk6XvW0v9q8Y66/YwNDH3rhj0jwDZhFvcy3yHBLonYgz39E3qBQLgvOzvp2/XAmhQpV2iu+WRFyh96RHll6L7lfgHmhHx3eVVkEbgnCpjm5CPiEk23FNQ901yuryHLHjWC0L0x+tcIhJdQ5+Rw5Vl/AyyYBUFK7OT2kmVuKTgFOTpnS8tb/uAgvorPuWHLhA5tfIuip8RKUFSQl7pWY4AYgfkooTjBvPKLhTkdaZUET6eg/MxSJ4PjHEjJEMR7s1mBSc3jWG2QThn+LLkarf0ycyo9Y+gW1iS5CKBljS9CdGqIx/zqQWs/tE8Cmo3T39PFm6EEAWXR0CsbihBDd+xgdF/b2bYwCYg0n7NpmrYILFBy4YxLyUdAiNdn/OPYB70IqZMk3xkMuoqqpVpqCeomwkm2uaNTG5OtzK3XXmuTx1L1+WrssHKUgMvnS8lBEkHcgGW066dRbNMtnYZ3BNj4cKLvLhKu92UAFMNj1+/AwjmmecnVAjDqMN99oMbiAzm+2hxmlDXO8UapD9atG8+ScjJY392iJOjWTTp6vLYmEh5y3EzRWZbSNJApWFCoOVnm4xn/7iSQ3PRbpyfeXUaGdS5QbUlQnWRzwKAE308fVKdyz4RX6e5Su1euzhBteeTawVk5jMaj7TSi5pQy+HbyXJtkNmmBiPDkG+CRtlZqbvKluLzzDD2UaAgPjXv9Z55434mETu+6RWe/Ex5/NmB1xRsyvuWfX5ewqw1u4m13vSN6nQ95ZT/0jzNJdY3kK3N8OBe9+ExklY4G/C1TWtzX7LLM22QKi11/KOl23m8v4TuMZgngMpfApchVfp5LVh48fUwWUiA8n1Cvq4G+lWR2aGYv6RpMSQwiWU1WzcAtLhyMNwLGpYrHTi0c2s7C8jeP0CIyE8jba+HjWeyLGzH0b9zU+GRXPXGelzhXG4/3IJH19UgiTgbTvmiGE9OYcxuah+WzgP3vIPFU2czb2H/H1oaBEWTnCpniV7/2d4ml06yxQ9bVHnuMU9nlSJtQ3yZcm8o0Fv02aqqyymK2k6ChfLCqBl7phkHOwYaGUj/bJDGsoa9yFvfl+6m+ikWMNKK1IahAlFD8KQyg9mLTNnpd6sYIfnBIkSagYNJ/HaZkiELdptL58UeF++kU0YPE6hrJ0FCs2A0s1o6O6U+BGmCS/UL3+qU65+fvwieXA8FL9OalS9njRcJKczpXZyV+yZyzGO+CnZ7sfA/BwLuYs85P6D935kGIE5A9bkn1yX3ngHVNpbEb+ybZdQp/62Jh6HAkVI/x+J5MclyrycRhAAN1DyqJnepcGCVd1dnpqF/wzyyTyOPxjYtcDF94zVfhsSBhet+lku6HQVJuCcvBxPRBfHzB6ehAGgHXHPbykc7AI8tNBmrgKrWi+y0zoaVp2JasGCvPWrn761owA0q84psYK0TIv4sZF8sL8dB4XZLTbTq0OBl6OJpZG9nFI4Gfy35Slu1r0MNTJ0XqJA7rm+loZ9oK8an3heaPJ9LIWBYumCFTVDPnG2hfLJIQGPy6m0r1f49YdYrvzQeqlkpi6FXCWLM7o3iiGftR8BExw1eVT60lnzhZWbOx5qRZLFrZOUczWhh1c3WpEG3HpXvogGmFy50XqTlY67Xqo88+sgpuONNh2XwhuGgwJFwG03YopDQfxWqGpKRrJpN2ERjW4obxz6jPa+EW3uxG1/X65YIlA7E+PctSonX1PdEsG0PXlJWshth1YU9Mj+sm2+eZmKOSM49AV6/P2niGeEhHs13PsJJvcMVnyJioBY1bbjvVzH8JzgpSFc1j1vSw+0QE+iXxCaWN/1OBLihNEgC+xCdtAe57X8D1ejEX+6k4UpvxN9OsO42pqht4dn/MHyxcu6mg9YkNfrc0GdSaQ6wZp23EXR4DVusDdamTnLQWT6r2PVx7OzYyz7vRCAk6AZfukPelgXKN4Um7JXWtHdGfUCl99AxgXFJciyXaf33KWytQktpRhx91x8UYuS+wL1fge0T0sTEY5XuB8PPQqz/Bxxr9GG3tzfetazE5aFtndaxvjgSQNNb9BdwPYx+NXltoSVBUavwYUucO8rd6cf0p+H0/Z+1hlV1vlzO7nk1WPyOe9W6F+UpZgUK0dTOPMN7QSEQ4IV2P6YVNzobwuA7IyePCFI9PdlrZR/1doBs1nG4+aIMggHq9ZiQg+pERHWEiofxPKPrX0VvEQ+rL3NuaLY0i6KL/ZQ3Z2XP0AoHgUwtTSoLhnwTp72fTVEYDWmgW8aZlaLNTI8LQGm9jq04Vcg1ABxnSUNBgRNpcP8zBdkaYQzuGG5Gd3H8y7/Eq9x2SqD7cyjakQnba6jM52X82RkxzP9mCuHzd1UNeVm3yECu3nqBiYN2XC/g5HIGto8Aj5btYAxWqTc23Hyc/YQ1AWeYR2rPc8GnZ0WNUFa0X+x60YJjcbpY4nR6IBY7Wh9/tVjkTirhIqQSlXViETBqW8Xb2wVUJ+s/luq6fOlC3p4yyYxUKuunLxml1DB0049T1b1M2VVRtT3vsvywc656mEPO0Ec+WGGx/SAqyuGR+aGNXz9QAwhBlX8WzWdsOMjlCsFLXHIcS19aoTe/E3R8R4AiVUZ2Dsvn1PoaKJhUGWYco/SRE2VF5UsPe6oS9TsVmAbneVQK3SxvqR5wDHyCHNAj6qRIc4jHeYHq+DzvcZgASgEmVcfUR/nSnXMBeAkduQqjWkr0+dyvZdQvy3+Qkwmls35MjRFkCIFMvkkM2JOS828ts2ce19ePy+a8uXpNMNeKgTK+RTBL7k9oPXu+v/XesQ4m+LnjAwzuEfX/9c2IRKbDJy4QyaSlcYXtzBKww7z+Frtioqy6zCgk9N/icIWTFIW+kimHP7uyfZ3+cd2amnl5IYLSTQWmdNJwQUiTxgxChj7/TC65gPVkLd9jElIVJcKWxbSZOKAi3DTrPD6PFx1w5HCsRw1eOz3AdDnNIbyIwroyO64mkgKaCfL+9rQH41UNJZhsY6mNwSahR9oTbf0Qefju83yZz1X1q5StqVk19O7F3VNpqZfik/T39cMdOWJg+PKvJID/pCFjxJ7eZFEnY/OWv27M+vNVlVGzLMCiO/HYab8Jc0aWhziFtycFVfi9obpO7Ug+vIrNZnRfSZbe0ONaKc1Rp3Z44xePLkLHeaM5+SqTOm5kunvzHGzVCBxzTZ7E/FvFigWt1/9bX2T/8FfcyUH7mxwgoOveCpMPJgUL1jvLeN7lFVwif4mxqJCy9MrK/ZTUB/MWb3BbvDnQdO9OeKzZnD9qyfW0A1/Alh7EM4kynSTEX2f1a4pFc4A4EphSx4g9YhVb6DK0qlEqzXY7MF7ykzkWNKc146MSwBLVpkBmGRP4jAv/0CA/FzLKsb994drvn+nDJMG1yrvOaEALkypUQlWExz3Hjglfzr6SFhULR6fW9CkBsRWGMMm6enOvmGjzrjvxQk4J8RpcFLdsWXTRiMmc3CBj8g+96LpA6jVcS0muAHrUU6MePcpnfGPDFFZzr8VRvJwFRuIaLh7j7r1sEiRxcC2cCznWxEQC+Yh7MwjoAQP7SG3IlbiXRTIRb5XiZXZ+SD7MEw822Z7ArQBvzqkriEz90yfYvboBKjKzGUy176O2c6ukJQJTVRmb/qpNzyW/IzwEl4Z7olsKlB/plITExDCNnncRMnBsiT+OXVDU4TzZ6AfRrjcfRvE2wMWSzdv1Hw9AaXrJN4OV1nqP6OI8ghptbFhmp9GA6ncsVsXNr8hVFN3noRiyFitjlJvzlVrQr23nRC9Bp9RRZ/njgQDMz28h9qfebas0m8zQf3zzw2R+8e2xoPYwrhpKRW97SetAAmxoxBbjqGVtZ5+q9PSDYCkQkePMfgATWRu1IvwdhZwRdSS5sz0cOlnx18NOXdu36a2F5PbEDj5K2cGbK2NbPqRV5wyzsSFiGRRDc5JtRzpl3TvlJa3peo+frTGQBqpOQNJa/rEiWgitDpYZjB3X/FQWQAAZtoGcrnWXPG8PzosGOimHVBcxrgUfWXF3LnL3IAMahrwCD2VHrbCVZyBBJSqIAwhp1xx5fk38TfTRZyP3wtyjbSk99vLPUOWAa5hhXMVHVOH9o/1ky1Ry7+s+dvgd9FXzv2gy7fuTWQToizUDfmQp1ntg2xCQ/wCHI7eL1g1jS4Rg9Toq34HlgKYLrjb9ERLxPtBSfBcQhF84wIiLZMcLH8E1NofbC0zzuVdK4BtWcVDkwlXlvZ7UAEf8ye+SQQq/j9a8CaNVIBfu/pi+iqMjpEcBDdlBifpAU/phS7WAqz4zlv/p2QyP5FACGt6ZFztu7/0FEVKGkEdcsvnh/6b2gkS/A676/7x7Q6wpUw6kbRSO57nl3WTGxHgUDARx/v4G06vRYRenCeP90km+edWzm0bCrjHcc8ucbfhVOvW5xb5sXXiTVMSoxLjlhnAVh3Es5Lh7AMeK4Hwvb/fWHSg2VYhNMUHDhfSuu6yHXPELNa5mfJ1rC35IgklfDVN7HPJRs/pMz1NLK9L1Qa1opsd4ZiTuQLKRnxWv6JaJU+HrZhZhHL00OUYLJu0QW5aitgrnCazt4QjRyL6MSF+E7IQh2LbAxo2TuOymXgsjywLZgVbsgS90auGXqSUMhDUsay7eNiPloMLLB/1nvlD5fe9eHggDtH58GmRVwmlSkC4Ht1q4lAjdDP1wnGbOh8DyDxBHiBKdewyOspq6Pi7YVIdkcM1hTKl8jWMVrq3V/4ssa1STza8z/wei6RW4+ozBywCwsJPD4NymeXRymMYIVoY+ckBq4uTV0/TyE1MX2iVjA5Mw3EynK0U+nny7kfw9xrIdNQgsp8UKHw99ryw+XfEi6gKO1m3xP2dBjG8kcMvxp2mgmRMdmjAie5wtPcLc/kY6mUJYJIcJ3/MGsgswWMF8kF3O1e7+gdRUmrd0gBNxVw+wWQX53+sCARtXhRdphWudN4H93ujCuZvOY/AXQKccHLBWwygqP+I3h3QUmy0aswokkMe5sbJSTOhX/qJrfONJu6265c5c5E8uJRPDtSMYbFIPzFnjpSiOtdZValJ7IJAuZXXHBxyoFdLVQRDgY95+B6QiNW7Mj6pjzYbv57cWRtRAY8iFqtNIkmq7AoqIgnWUkzKxistJIs+L+fa3GIOa/l7O75WBsAGRXsUJWnfdW5JwYh7KZqhe/seJfogFSfX44CAQCpMaG0kAr2W9GBT8zKkB0TUrXmzn1BxtGPp+XSYPAk/Zuu3Agn62KXdwr7NjhFO0eo4dLa1Qj7XqOKgDP3JxDKNQclMw7C7vAOevFpQFC8MTH4GUADHNZ84OGNQuswzUdqvgWnZwo6fk0nYNByExnylN1MGSvMJLABh8SAD65LtfTxgcBtQWZx/q0+VYVx7ObUAbc2qR7Vb5NTr+YLTf+y31u3MvFmehpH4yoY+VDReaG/RElC53WVPdONW9hoKCNbvL+ko1T34s2BTpAONNxval9trEfu3HvV9riJlpgQPcTY0vV5GyAaqse4AwBfzz9/af6qOG5d7Ky9Tk/1Te1ogEhQUSxzXUUtau4qDKaIpkpq+djyWAvGe7Rq83igFp3YxKOwzP9Sx88nNbghn/ZuZbU1yBJ07OLX7j/+1IYia+rtYBdk00nq/BAlQ2+HEVU1lY04Fk1YLymlwWGlox5DP8PUnaKdf85EAgklj4mWsmAuQHHRf8Hib7TE+fNcbCiH6sj2O/nbLAa1xUysGFEtr8UFB7E2FKU7dRRdhKUsuuTAUuEX3gizNcfZxu3SGY5BzyrzNq3MwO/QAmERYr21px71G7REsMecDeZC/9Z2vxz2lo9uGwF3mjbpEf/GGdWaSIt+2p6mw6/vqEBsQRgjT5OinhvyaCd2QlTK+Vie54YcXquvr4O6vBUMvPAJgROh16EYqstFYlVf/DbSdPN5UCtgxTifdKmODGv/J/fgLbwxNvgZOAbvvaBySDPGfU2vAHWrBp5aVJEMUWt7QKymzpPP43sb3NZIugjhw3IAr5jgU4tweOBLQ8dNHN1aMAwPOlFI9mnLCoeC41hzE+wyDydtqD5kd0vYqU4jciEbSPlbSIpz0xvW2q1aI6epjF4dPBTi4k0/h6qCLVLaED2ISLy0gy0C5ds/qkFiLxqarpMXV+lDPwaK9jx4I/ta+o6Z0o+lmRFv+TR8AzDPciZi9mih4hu2HQOiC0S7bRMqmkp08np+4GSrRsIwFThxgcXgufXb/z38v26Nf92dSnHOl6rkSJkPyEvkG8TCaxLUXBL+PVDNF86YuHZYhC2CZkciAoDcG/UO+zts6VUqW9RGJ/0PLnL6aGRvls1WxxzfKWuls/QwpeQtsnKkOTMErgWb3AtNI0WM6GX0xc5pnbKpn0w9vAkSSDabsitdfQhm2CGQtFb9uKYOh2ncfY8Xl6rPKXJl+K3b3xiiBU608SBbKBiR319b+E4zlyMZciVNnHEbhPv4WG7nOcZ71xgb63E3dvD4c0Upr61vWthQNc9WoX1BqGUpQwJisyvuS3X4egeMius0UjKWfuyFuSLsAsvQCwPoyqmSt9q1VvYjp5YauEgLvvPvBR4vjQ6Hb0hwwMwbMCJIYX8Sa5MuHTaKsqmQF5iHD/3q/A69QGNbPw5Otvt/HqOuzoPNvmBzw0vkpbnaRKPTlkEXgByj288jl9EM5LHz625Z4gSz2PiqQ3qAc3gnaG9sC3UYpo0O84ogxtZeX9gD8lJkZCDJGNnEKmK9Cmgc/Ye/DEGj26Y4aOvpGND/k/qjhKpKZaO8b7L+qd1oI0vUAoeTwlCqssWXKI0SwPjmzLfGIGRllpEhRb89qSf5U1yTZVox5aDx5h5aIsUBjJhOIM+Ram9+07vstlEvhykxZGwRCD9vAnsQ/iDrb0/4PLIbMAvgcAkZytMTIdEg8G8WupHM+9AAhbDfye9+tijNQkomcVyvDVn0VZgWGNuVFo8wWoH6ata3sDVLcGIgSZ8Ff1BR1aE7A/m20xoWXr+TxzNmQVrbOnU5qu30YJNss3zhryVNYElg2+d+0DrqEiB0TL7RBiRvYf9dqoigzSX8s+gx234otufbeBA8KHuHTqLltn0/sNEnOkl2GeTmd2IVIMh3Tw2QCWNUnaIFK0ZytpxeeTos8QhLvvB/HF/MezMewVmwpOxV/5d9DapJyw40qxG5ITlCVxd6nfn+PrusOE7I/G9uJiNyPBiorveaFrs8EcUK33rL9tJSXaVfK8OVsBzRC6UmykhNNZwhGUICLGszb4X4bTlSxWq4Sn+JGHt1lODp1cmwtbGlzdGwzMTpodHRwczovL3dlYnRvcnJlbnQuaW8vdG9ycmVudHMvZWU=", + }; + + var add_torrent = try transmission.torrentAdd(&client, add_torrent_request); + defer add_torrent.deinit(); + + try testing.expectEqualStrings("success", add_torrent.data.TorrentAdd.result); + try testing.expectEqualStrings( + "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c", + add_torrent.data.TorrentAdd.arguments.@"torrent-added".?.hashString, + ); + try testing.expectEqualStrings( + "Big Buck Bunny", + add_torrent.data.TorrentAdd.arguments.@"torrent-added".?.name, + ); + try testing.expect( + 1 == add_torrent.data.TorrentAdd.arguments.@"torrent-added".?.id, + ); + + var get_torrent_null = try transmission.torrentGet(&client, null); + defer get_torrent_null.deinit(); + + try testing.expectEqualStrings("success", get_torrent_null.data.TorrentGet.result); + try testing.expect(1 == get_torrent_null.data.TorrentGet.arguments.torrents.?.len); + try testing.expectEqualStrings( + "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c", + get_torrent_null.data.TorrentGet.arguments.torrents.?[0].hashString.?, + ); + try testing.expectEqualStrings( + "Big Buck Bunny", + get_torrent_null.data.TorrentGet.arguments.torrents.?[0].name.?, + ); + try testing.expect( + 1 == get_torrent_null.data.TorrentGet.arguments.torrents.?[0].id.?, + ); +} diff --git a/src/transmission.zig b/src/transmission.zig index 9f0f5f0..239151a 100644 --- a/src/transmission.zig +++ b/src/transmission.zig @@ -3,8 +3,8 @@ const http = std.http; const util = @import("util.zig"); -const Request = @import("request.zig"); -const Response = @import("response.zig"); +pub const Request = @import("request.zig"); +pub const Response = @import("response.zig").Response; pub const ClientOptions = extern struct { host: [*:0]const u8, @@ -100,58 +100,92 @@ pub const Client = struct { } }; -pub fn session_get_raw(client: *Client, session_get: ?Request.SessionGet) ![]u8 { - const default: Request.SessionGet = .{ - .fields = Request.SessionGet.all_fields, - }; - - const r = Request.Object{ - .method = .session_get, - .arguments = .{ .session_get = session_get orelse default }, - }; - const body = try client.do(r); - return body; +pub fn torrentAdd(client: *Client, r: Request.TorrentAdd) !Response { + const body = try torrentAddRaw(client, r); + defer client.allocator.free(body); + return Response.parseBody( + client.allocator, + Request.Method.@"torrent-add", + body, + null, + ); } -pub fn torrent_get_raw(client: *Client, torrent_get: ?Request.TorrentGet) ![]u8 { - const default: Request.TorrentGet = .{ - .fields = Request.TorrentGet.all_fields, - }; - //const default: Request.TorrentGet = .{ - //.fields = &[_]Request.TorrentGet.Fields{.name}, - //}; - - const r = Request.Object{ - .method = .torrent_get, - .arguments = .{ .torrent_get = torrent_get orelse default }, - }; - const body = try client.do(r); - std.debug.print("{s}\n", .{body}); - if (1 == 1) - @panic(""); +pub fn torrentAddRaw(client: *Client, r: Request.TorrentAdd) ![]u8 { + const body = try client.do(Request.Object{ + .method = .@"torrent-add", + .arguments = .{ .@"torrent-add" = r }, + }); return body; } -pub fn torrent_get_(client: *Client, torrent_get: ?Request.TorrentGet) !Response.Object { - @setEvalBranchQuota(100000); - const body = try torrent_get_raw(client, torrent_get); - const decoded: Response.TorrentGet = try std.json.parseFromSlice( - Response.TorrentGet, +pub fn torrentGet(client: *Client, r: ?Request.TorrentGet) !Response { + const body = try torrentGetRaw(client, r); + defer client.allocator.free(body); + return Response.parseBody( client.allocator, + Request.Method.@"torrent-get", body, - std.json.ParseOptions{ - .ignore_unknown_fields = true, - .duplicate_field_behavior = .@"error", - }, + null, ); - return Response.Object.init(.{ .torrent_get = decoded.arguments }, decoded.result); } -pub fn session_set_raw(client: *Client, session_set: Request.SessionSet) ![]u8 { - const r = Request.Object{ - .method = .session_set, - .arguments = .{ .session_set = session_set }, +pub fn torrentGetRaw(client: *Client, r: ?Request.TorrentGet) ![]u8 { + const request = blk: { + if (r == null) { + break :blk Request.Object{ + .method = .@"torrent-get", + .arguments = .{ + .@"torrent-get" = Request.TorrentGet{ + .fields = Request.TorrentGet.all_fields, + }, + }, + }; + } else { + break :blk Request.Object{ + .method = .@"torrent-get", + .arguments = .{ .@"torrent-get" = r.? }, + }; + } }; - const body = try client.do(r); + + const body = try client.do(request); return body; } + +//pub fn session_get_raw(client: *Client, session_get: ?Request.SessionGet) ![]u8 { +//const default: Request.SessionGet = .{ +//.fields = Request.SessionGet.all_fields, +//}; + +//const r = Request.Object{ +//.method = .session_get, +//.arguments = .{ .session_get = session_get orelse default }, +//}; +//const body = try client.do(r); +//return body; +//} + +//pub fn torrent_get_(client: *Client, torrent_get: ?Request.TorrentGet) !Response.Object { +//@setEvalBranchQuota(100000); +//const body = try torrent_get_raw(client, torrent_get); +//const decoded: Response.TorrentGet = try std.json.parseFromSlice( +//Response.TorrentGet, +//client.allocator, +//body, +//std.json.ParseOptions{ +//.ignore_unknown_fields = true, +//.duplicate_field_behavior = .@"error", +//}, +//); +//return Response.Object.init(.{ .torrent_get = decoded.arguments }, decoded.result); +//} + +//pub fn session_set_raw(client: *Client, session_set: Request.SessionSet) ![]u8 { +//const r = Request.Object{ +//.method = .session_set, +//.arguments = .{ .session_set = session_set }, +//}; +//const body = try client.do(r); +//return body; +//} diff --git a/src/types.zig b/src/types.zig index 7de8fa1..7ed50dc 100644 --- a/src/types.zig +++ b/src/types.zig @@ -1,3 +1,7 @@ +const std = @import("std"); + +const Request = @import("request.zig").Request; + // types are from: // https://github.com/transmission/transmission/blob/76166d8fa71f351fe46221d737f8849b23f551f7/libtransmission/transmission.h#L1604 // where: @@ -225,3 +229,44 @@ pub const Torrent = struct { // Number of webseeds that are sending data to us. webseedsSendingToUs: ?u16 = null, }; + +// Checks that all fields in Torrent are in sync with TorrentGet.Fields +test "torrent fields" { + @setEvalBranchQuota(10000); + + const torrent_get_request_fields = comptime blk: { + const kv = struct { []const u8 }; + var slice: []const kv = &[_]kv{}; + + for (std.meta.fields(Request.TorrentGet.Fields)) |field| { + slice = slice ++ &[_]kv{.{field.name}}; + } + + break :blk std.ComptimeStringMap(void, slice); + }; + + const torrent_fields = comptime blk: { + const kv = struct { []const u8 }; + var slice: []const kv = &[_]kv{}; + + for (std.meta.fields(Torrent)) |field| { + slice = slice ++ &[_]kv{.{field.name}}; + } + + break :blk std.ComptimeStringMap(void, slice); + }; + + inline for (std.meta.fields(Torrent)) |field| { + if (!torrent_get_request_fields.has(field.name)) { + std.debug.print("field is present in Torrent but missing in TorrentGet: {s}\n", .{field.name}); + } + //try std.testing.expect(torrent_get_request_fields.has(field.name)); + } + + inline for (std.meta.fields(Request.TorrentGet.Fields)) |field| { + if (!torrent_fields.has(field.name)) { + std.debug.print("field is present in TorrentGet but missing in Torrent: {s}\n", .{field.name}); + } + //try std.testing.expect(torrent_fields.has(field.name)); + } +} diff --git a/src/util.zig b/src/util.zig index c46dbe7..07210b2 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,108 +1,108 @@ const std = @import("std"); -const struct_util = @import("util/struct.zig"); +//const struct_util = @import("util/struct.zig"); const enum_util = @import("util/enum.zig"); pub const enumFieldsSlice = enum_util.enumFieldsSlice; // Builds a static map of fields in `T` using `F`. // to be used by our stringify -pub fn JsonMap( - comptime T: type, - comptime F: ?fn (comptime []const u8) []const u8, -) type { - return blk: { - switch (@typeInfo(T)) { - .Enum => { - break :blk struct { - const fields = enum_util.enumFieldsStringSlice(T, F); - pub fn get(field: T) []const u8 { - return fields[@enumToInt(field)]; - } - }; - }, - .Struct => { - break :blk struct { - const map = struct_util.StructFieldsMap( - T, - struct_util.StructFieldsEnum(T, F), - ); - pub fn get(field: []const u8) []const u8 { - return @tagName(map.get(field).?); - } - }; - }, - else => unreachable, - } - }; -} +//pub fn JsonMap( +//comptime T: type, +//comptime F: ?fn (comptime []const u8) []const u8, +//) type { +//return blk: { +//switch (@typeInfo(T)) { +//.Enum => { +//break :blk struct { +//const fields = enum_util.enumFieldsStringSlice(T, F); +//pub fn get(field: T) []const u8 { +//return fields[@enumToInt(field)]; +//} +//}; +//}, +//.Struct => { +//break :blk struct { +//const map = struct_util.StructFieldsMap( +//T, +//struct_util.StructFieldsEnum(T, F), +//); +//pub fn get(field: []const u8) []const u8 { +//return @tagName(map.get(field).?); +//} +//}; +//}, +//else => unreachable, +//} +//}; +//} // copy of std.json.stringify to change field names // using JsonMap when serializing -pub fn jsonStringify( - value: anytype, - options: std.json.StringifyOptions, - out_stream: anytype, -) !void { - const T = @TypeOf(value); - switch (@typeInfo(T)) { - .Enum => { - const member_name = T.json_map.get(value); - try std.json.stringify(member_name, options, out_stream); - }, - .Struct => |S| { - try out_stream.writeByte(if (S.is_tuple) '[' else '{'); - var field_output = false; - var child_options = options; - child_options.whitespace.indent_level += 1; - inline for (S.fields) |Field| { - // don't include void fields - if (Field.type == void) continue; +//pub fn jsonStringify( +//value: anytype, +//options: std.json.StringifyOptions, +//out_stream: anytype, +//) !void { +//const T = @TypeOf(value); +//switch (@typeInfo(T)) { +//.Enum => { +//const member_name = T.json_map.get(value); +//try std.json.stringify(member_name, options, out_stream); +//}, +//.Struct => |S| { +//try out_stream.writeByte(if (S.is_tuple) '[' else '{'); +//var field_output = false; +//var child_options = options; +//child_options.whitespace.indent_level += 1; +//inline for (S.fields) |Field| { +//// don't include void fields +//if (Field.type == void) continue; - var emit_field = true; +//var emit_field = true; - // don't include optional fields that are null when emit_null_optional_fields is set to false - if (@typeInfo(Field.type) == .Optional) { - if (options.emit_null_optional_fields == false) { - if (@field(value, Field.name) == null) { - emit_field = false; - } - } - } +//// don't include optional fields that are null when emit_null_optional_fields is set to false +//if (@typeInfo(Field.type) == .Optional) { +//if (options.emit_null_optional_fields == false) { +//if (@field(value, Field.name) == null) { +//emit_field = false; +//} +//} +//} - if (emit_field) { - if (!field_output) { - field_output = true; - } else { - try out_stream.writeByte(','); - } - try child_options.whitespace.outputIndent(out_stream); - if (!S.is_tuple) { - try std.json.encodeJsonString( - T.json_map.get(Field.name), - options, - out_stream, - ); - try out_stream.writeByte(':'); - if (child_options.whitespace.separator) { - try out_stream.writeByte(' '); - } - } - try std.json.stringify(@field(value, Field.name), child_options, out_stream); - } - } - if (field_output) { - try options.whitespace.outputIndent(out_stream); - } - try out_stream.writeByte(if (S.is_tuple) ']' else '}'); - return; - }, - else => unreachable, - } -} +//if (emit_field) { +//if (!field_output) { +//field_output = true; +//} else { +//try out_stream.writeByte(','); +//} +//try child_options.whitespace.outputIndent(out_stream); +//if (!S.is_tuple) { +//try std.json.encodeJsonString( +//T.json_map.get(Field.name), +//options, +//out_stream, +//); +//try out_stream.writeByte(':'); +//if (child_options.whitespace.separator) { +//try out_stream.writeByte(' '); +//} +//} +//try std.json.stringify(@field(value, Field.name), child_options, out_stream); +//} +//} +//if (field_output) { +//try options.whitespace.outputIndent(out_stream); +//} +//try out_stream.writeByte(if (S.is_tuple) ']' else '}'); +//return; +//}, +//else => unreachable, +//} +//} -pub fn replaceUnderscores(comptime o: []const u8) []const u8 { - @setEvalBranchQuota(10000); - var name: [o.len]u8 = undefined; - _ = std.mem.replace(u8, o, &[_]u8{'_'}, &[_]u8{'-'}, &name); - return &name; -} +//pub fn replaceUnderscores(comptime o: []const u8) []const u8 { +//@setEvalBranchQuota(10000); +//var name: [o.len]u8 = undefined; +//_ = std.mem.replace(u8, o, &[_]u8{'_'}, &[_]u8{'-'}, &name); +//return &name; +//} |