diff --git a/.gitignore b/.gitignore index 530a19c..65f6984 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ __pycache__ /.coverage /.zig-cache /zig-out +/zig-pkg +*.egg-info diff --git a/build.zig b/build.zig index 1af6a61..7a1aab1 100644 --- a/build.zig +++ b/build.zig @@ -3,18 +3,12 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const jtftp = b.addModule("jtftp", .{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); - const netlink_dep = b.dependency( - "netlink", - .{ - .optimize = optimize, - .target = target, - }, - ); const exe = b.addExecutable(.{ .name = "jtftp", @@ -24,7 +18,6 @@ pub fn build(b: *std.Build) void { .optimize = optimize, .imports = &.{ .{ .name = "jtftp", .module = jtftp }, - .{ .name = "netlink", .module = netlink_dep.module("netlink") }, }, }), }); diff --git a/build.zig.zon b/build.zig.zon index 80d106e..ff02909 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,12 +2,12 @@ .name = .jtftp, .version = "0.0.0", .fingerprint = 0xa6f1d5663162eacf, // Changing this has security and trust implications. - .minimum_zig_version = "0.15.1", + .minimum_zig_version = "0.16.0-dev.2694+74f361a5c", .dependencies = .{ - .netlink = .{ - .url = "https://git.0x1b.me/netlink/archive/abbf6720e422f735d1c2612698363ce30e3dae47.tar.gz", - .hash = "netlink-0.1.0-9us2qXspAQBim0YBEaJd5hwvf8nc7sFpqufX-vtYle61", - }, + // .netlink = .{ + // .url = "https://git.0x1b.me/netlink/archive/abbf6720e422f735d1c2612698363ce30e3dae47.tar.gz", + // .hash = "netlink-0.1.0-9us2qXspAQBim0YBEaJd5hwvf8nc7sFpqufX-vtYle61", + // }, }, .paths = .{ "build.zig", diff --git a/flake.lock b/flake.lock index ec319f8..1bfdf53 100644 --- a/flake.lock +++ b/flake.lock @@ -2,17 +2,43 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1757808699, - "narHash": "sha256-rmyZ6B4DtU9MwkBSEf63NU4czFoM0budKcAtlhjaGEc=", - "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", + "lastModified": 1778869304, + "narHash": "sha256-VdRy3A14M5vIE882DJcaaR+5wrss9Qsg4YNVbr7uj3k=", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre861038.c23193b943c6/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre998534.d233902339c0/nixexprs.tar.xz" }, "original": { "type": "tarball", "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" } }, + "pyproject-build-systems": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ], + "uv2nix": [ + "uv2nix" + ] + }, + "locked": { + "lastModified": 1776659114, + "narHash": "sha256-qapCOQmR++yZSY43dzrp3wCrkOTLpod+ONtJWBk6iKU=", + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "rev": "ffaa2161dd5d63e0e94591f86b54fc239660fb2e", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "build-system-pkgs", + "type": "github" + } + }, "pyproject-nix": { "inputs": { "nixpkgs": [ @@ -20,11 +46,11 @@ ] }, "locked": { - "lastModified": 1757246327, - "narHash": "sha256-6pNlGhwOIMfhe/RLjHdpXveKS4FyLHvlGe+KtjDild4=", + "lastModified": 1778901413, + "narHash": "sha256-GSKXTAnFqRAMlZkJrIPcQMYf+lpMr66K3i60mB9STvc=", "owner": "pyproject-nix", "repo": "pyproject.nix", - "rev": "8d77f342d66ad1601cdb9d97e9388b69f64d4c8e", + "rev": "a228447c3e179d477c1b6246ef3efa8cfe3c469a", "type": "github" }, "original": { @@ -36,7 +62,32 @@ "root": { "inputs": { "nixpkgs": "nixpkgs", - "pyproject-nix": "pyproject-nix" + "pyproject-build-systems": "pyproject-build-systems", + "pyproject-nix": "pyproject-nix", + "uv2nix": "uv2nix" + } + }, + "uv2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "pyproject-nix": [ + "pyproject-nix" + ] + }, + "locked": { + "lastModified": 1778664018, + "narHash": "sha256-ogNyNANNLo0SMFevIeUpbTMOL9uUDu/hXvp7JlOYbwQ=", + "owner": "pyproject-nix", + "repo": "uv2nix", + "rev": "b48abe99ef639cd100c224898529370e5d935294", + "type": "github" + }, + "original": { + "owner": "pyproject-nix", + "repo": "uv2nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 4a31014..05e648d 100644 --- a/flake.nix +++ b/flake.nix @@ -7,76 +7,85 @@ url = "github:pyproject-nix/pyproject.nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + uv2nix = { + url = "github:pyproject-nix/uv2nix"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + pyproject-build-systems = { + url = "github:pyproject-nix/build-system-pkgs"; + inputs.pyproject-nix.follows = "pyproject-nix"; + inputs.uv2nix.follows = "uv2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { self, nixpkgs, + pyproject-build-systems, pyproject-nix, + uv2nix, ... }: let - project = pyproject-nix.lib.project.loadPyproject { - projectRoot = ./.; - }; + platforms = [ "x86_64-linux" ]; packages = system: import nixpkgs { inherit system; }; - forAllSystems = ( - function: - nixpkgs.lib.genAttrs [ - "aarch64-linux" - "aarch64-darwin" - "x86_64-linux" - "x86_64-darwin" - ] (system: function (packages system)) - ); + forAllSystems = (function: nixpkgs.lib.genAttrs platforms (system: function (packages system))); in { - devShells = forAllSystems ( - pkgs: - let - python = pkgs.python3; - arg = project.renderers.withPackages { inherit python; }; - pythonEnv = python.withPackages arg; - in - { - zig_0_15 = pkgs.mkShell { - packages = [ - pythonEnv - pkgs.tftp-hpa - pkgs.uv - pkgs.zig_0_15 - ]; - }; - default = self.devShells.${pkgs.system}.zig_0_15; - } - ); - packages = forAllSystems ( - pkgs: - let - python = pkgs.python3; - in - { - jtftp = - let - attrs = project.renderers.buildPythonPackage { inherit python; }; - in - python.pkgs.buildPythonPackage ( - attrs - // { - } + devShells = forAllSystems (pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.tftp-hpa + pkgs.uv + pkgs.zig_0_16 + ]; + UV_NO_MANAGED_PYTHON = "true"; + UV_PYTHON = pkgs.lib.getExe pkgs.python3; + shellHook = '' + export VIRTUAL_ENV=''$(pwd)/.venv + export PATH=''$(pwd)/.venv/bin:$PATH + ''; + }; + }); + packages = forAllSystems (pkgs: { + jtftp = + let + python = pkgs.python3; + workspace = uv2nix.lib.workspace.loadWorkspace { + workspaceRoot = ./.; + }; + overlay = workspace.mkPyprojectOverlay { + sourcePreference = "wheel"; + }; + baseSet = pkgs.callPackage pyproject-nix.build.packages { + inherit python; + }; + pythonSet = baseSet.overrideScope ( + pkgs.lib.composeManyExtensions [ + pyproject-build-systems.overlays.default + overlay + ] ); - default = self.packages.${pkgs.system}.jtftp; - } - ); + package = pythonSet.jtftp; + venv = pythonSet.mkVirtualEnv (package.pname + "-env") workspace.deps.default; + inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication; + in + mkApplication { + inherit venv package; + }; + default = self.packages.${pkgs.stdenv.hostPlatform.system}.jtftp; + }); apps = forAllSystems (pkgs: { default = { type = "app"; - program = "${self.packages.${pkgs.system}.jtftp}/bin/serve"; + program = pkgs.lib.getExe' self.packages.${pkgs.stdenv.hostPlatform.system}.jtftp "serve"; }; }); }; diff --git a/pyproject.toml b/pyproject.toml index 0531a62..a22ef64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,12 @@ name = "jtftp" version = "0.1.0" description = "Jeff's TFTP" -# authors = ["Jeffrey C. Ollie "] +requires-python = ">= 3.11" dependencies = ["arrow ~= 1.3.0", "aiofiles ~= 24.1.0"] [project.scripts] serve = "examples.main:serve" + +[tool.uv] +package = true diff --git a/src/lib/Options.zig b/src/lib/Options.zig new file mode 100644 index 0000000..cd9da57 --- /dev/null +++ b/src/lib/Options.zig @@ -0,0 +1,118 @@ +pub const Options = @This(); + +const std = @import("std"); +const testing = std.testing; + +blocksize: ?u16 = null, +timeout: ?u8 = null, +transfer_size: ?u64 = null, + +pub fn decode(input: *std.Io.Reader) !Options { + var self: Options = .{}; + options: while (true) { + const option_str = input.peekDelimiterExclusive(0) catch |err| switch (err) { + error.StreamTooLong => break :options, + error.EndOfStream => break :options, + else => |e| return e, + }; + input.toss(option_str.len + 1); + const value_str = input.peekDelimiterExclusive(0) catch |err| switch (err) { + error.StreamTooLong => break :options, + error.EndOfStream => break :options, + else => |e| return e, + }; + input.toss(value_str.len + 1); + const option = Option.decode(option_str) orelse continue :options; + switch (option) { + .blksize => { + const value = std.fmt.parseUnsigned(u16, value_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => continue :options, + }; + if (value < 8 or value > 65464) continue :options; + self.blocksize = value; + }, + .timeout => { + const value = std.fmt.parseUnsigned(u8, value_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => continue :options, + }; + if (value < 1) continue :options; + self.timeout = value; + }, + .tsize => { + const value = std.fmt.parseUnsigned(u64, value_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => continue :options, + }; + self.transfer_size = value; + }, + } + } + + return self; +} + +pub fn encode(self: Options, output: *std.Io.Writer) !void { + if (self.blocksize) |blocksize| { + try output.writeAll("blksize"); + try output.writeByte(0); + try output.print("{d}", .{blocksize}); + try output.writeByte(0); + } + if (self.timeout) |timeout| { + try output.writeAll("timeout"); + try output.writeByte(0); + try output.print("{d}", .{timeout}); + try output.writeByte(0); + } + if (self.transfer_size) |transfer_size| { + try output.writeAll("tsize"); + try output.writeByte(0); + try output.print("{d}", .{transfer_size}); + try output.writeByte(0); + } +} + +pub fn empty(self: Options) bool { + const fields = @typeInfo(Options).@"struct".fields; + inline for (fields) |field| { + if (@field(self, field.name) != null) return false; + } + return true; +} + +pub const Option = enum { + blksize, + timeout, + tsize, + + pub fn decode(data: []const u8) ?Option { + inline for (@typeInfo(Option).@"enum".fields) |field| + if (std.ascii.eqlIgnoreCase(field.name, data)) + return @field(Option, field.name); + return null; + } + + test "option 1" { + try testing.expectEqual(.blksize, Option.decode("blksize")); + try testing.expectEqual(.blksize, Option.decode("BLKSIZE")); + try testing.expectEqual(.blksize, Option.decode("Blksize")); + try testing.expectEqual(.blksize, Option.decode("BlkSize")); + } + + test "option 2" { + try testing.expectEqual(.timeout, Option.decode("timeout")); + try testing.expectEqual(.timeout, Option.decode("TIMEOUT")); + try testing.expectEqual(.timeout, Option.decode("Timeout")); + try testing.expectEqual(.timeout, Option.decode("TimeOut")); + } + + test "option 3" { + try testing.expectEqual(.tsize, Option.decode("tsize")); + try testing.expectEqual(.tsize, Option.decode("TSIZE")); + try testing.expectEqual(.tsize, Option.decode("Tsize")); + try testing.expectEqual(.tsize, Option.decode("TSize")); + } + + test "option 4" { + try testing.expect(Option.decode("apple") == null); + } +}; diff --git a/src/lib/errorcode.zig b/src/lib/errorcode.zig new file mode 100644 index 0000000..fa0f216 --- /dev/null +++ b/src/lib/errorcode.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub const ErrorCode = enum(u16) { + not_defined = 0, + file_not_found = 1, + access_violation = 2, + disk_full = 3, + illegal_operation = 4, + tid_unknown = 5, + file_exists = 6, + no_such_user = 7, + term_option = 8, + _, + + pub fn decode(input: *std.Io.Reader) !ErrorCode { + return try input.takeEnum(ErrorCode, .big); + } + + pub fn encode(self: ErrorCode, output: *std.Io.Writer) !void { + try output.writeInt(u16, @intFromEnum(self), .big); + } + + pub fn message(self: ErrorCode) []const u8 { + return switch (self) { + .not_defined => "", + .file_not_found => "File not found", + .access_violation => "Access violation", + .disk_full => "Disk full or allocation exceeded", + .illegal_operation => "Illegal TFTP operation", + .tid_unknown => "Unknown transfer ID", + .file_exists => "File already exists", + .no_such_user => "No such user", + .term_option => "Terminate transfer due to option negotiation", + _ => "", + }; + } +}; diff --git a/src/lib/mode.zig b/src/lib/mode.zig new file mode 100644 index 0000000..f98b59c --- /dev/null +++ b/src/lib/mode.zig @@ -0,0 +1,73 @@ +const std = @import("std"); +const testing = std.testing; +const Allocator = std.mem.Allocator; + +const netascii = @import("netascii.zig"); + +pub const Mode = enum { + pub const DecodeErrors = error{ + InvalidMode, + } || netascii.DecodeSentinelErrors; + + mail, + netascii, + octet, + + pub fn decodeZ(input: *std.Io.Reader) Mode.DecodeErrors!Mode { + var buf: [16]u8 = undefined; + var output: std.Io.Writer = .fixed(&buf); + + _ = try netascii.decodeZ(input, &output); + + inline for (@typeInfo(Mode).@"enum".fields) |field| + if (std.ascii.eqlIgnoreCase(field.name, output.buffered())) + return @field(Mode, field.name); + + return error.InvalidMode; + } + + pub fn encodeZ(self: Mode, output: *std.Io.Writer) !void { + var input: std.Io.Reader = .fixed(@tagName(self)); + try netascii.encodeZ(&input, output); + } + + test "mode 1" { + { + var input: std.Io.Reader = .fixed("mail\x00"); + try testing.expectEqual(.mail, Mode.decodeZ(&input)); + } + { + var input: std.Io.Reader = .fixed("MAIL\x00"); + try testing.expectEqual(.mail, Mode.decodeZ(&input)); + } + { + var input: std.Io.Reader = .fixed("Mail\x00"); + try testing.expectEqual(.mail, Mode.decodeZ(&input)); + } + { + var input: std.Io.Reader = .fixed("MaIl\x00"); + try testing.expectEqual(.mail, Mode.decodeZ(&input)); + } + } + + test "mode 2" { + { + var input: std.Io.Reader = .fixed("netascii\x00"); + try testing.expectEqual(.netascii, Mode.decodeZ(&input)); + } + } + + test "mode 3" { + { + var input: std.Io.Reader = .fixed("octet\x00"); + try testing.expectEqual(.octet, Mode.decodeZ(&input)); + } + } + + test "mode 4" { + { + var input: std.Io.Reader = .fixed("apple\x00"); + try testing.expectError(error.InvalidMode, Mode.decodeZ(&input)); + } + } +}; diff --git a/src/lib/netascii.zig b/src/lib/netascii.zig index fc9c60e..d5456a3 100644 --- a/src/lib/netascii.zig +++ b/src/lib/netascii.zig @@ -20,15 +20,15 @@ pub const LineSeparator = enum { crlf, }; -pub fn decode(input: *std.io.Reader, output: *std.Io.Writer) DecodeErrors!void { +pub fn decode(input: *std.Io.Reader, output: *std.Io.Writer) DecodeErrors!usize { switch (builtin.os.tag) { - .windows => _decode(input, output, null, .crlf) catch |err| { + .windows => return _decode(input, output, null, .crlf) catch |err| { switch (err) { error.MissingSentinel => unreachable, else => |e| return e, } }, - else => _decode(input, output, null, .lf) catch |err| { + else => return _decode(input, output, null, .lf) catch |err| { switch (err) { error.MissingSentinel => unreachable, else => |e| return e, @@ -38,15 +38,15 @@ pub fn decode(input: *std.io.Reader, output: *std.Io.Writer) DecodeErrors!void { } } -pub fn decodeZ(input: *std.Io.Reader, output: *std.Io.Writer) DecodeSentinelErrors!void { +pub fn decodeZ(input: *std.Io.Reader, output: *std.Io.Writer) DecodeSentinelErrors!usize { switch (builtin.os.tag) { - .windows => try _decode(input, output, std.ascii.control_code.nul, .crlf), - else => try _decode(input, output, std.ascii.control_code.nul, .lf), + .windows => return try _decode(input, output, std.ascii.control_code.nul, .crlf), + else => return try _decode(input, output, std.ascii.control_code.nul, .lf), // macOS before OS X would be .cr, but that is not supported by Zig } } -fn _decode(input: *std.Io.Reader, output: *std.Io.Writer, comptime sentinel_: ?u8, comptime linesep: LineSeparator) DecodeSentinelErrors!void { +fn _decode(input: *std.Io.Reader, output: *std.Io.Writer, comptime sentinel_: ?u8, comptime linesep: LineSeparator) DecodeSentinelErrors!usize { const replacements = .{ .{ [_]u8{ std.ascii.control_code.cr, std.ascii.control_code.nul }, @@ -62,7 +62,9 @@ fn _decode(input: *std.Io.Reader, output: *std.Io.Writer, comptime sentinel_: ?u }, }; - while (true) { + var count: usize = 0; + + outer: while (true) { inline for (replacements) |item| replacement: { const needle, const replacement = item; const data = input.peek(needle.len) catch |err| switch (err) { @@ -71,24 +73,32 @@ fn _decode(input: *std.Io.Reader, output: *std.Io.Writer, comptime sentinel_: ?u }; if (std.mem.eql(u8, data, &needle)) { input.toss(needle.len); + count += needle.len; try output.writeAll(&replacement); + continue :outer; } } const byte = input.takeByte() catch |err| switch (err) { error.EndOfStream => { if (sentinel_) |_| return error.MissingSentinel; - return; + return count; }, error.ReadFailed => |e| return e, }; + count += 1; switch (byte) { - std.ascii.control_code.cr => return error.UnescapedCR, + std.ascii.control_code.cr => { + std.log.warn("unescaped cr at {d}", .{count}); + return error.UnescapedCR; + }, else => { - if (sentinel_) |sentinel| if (byte == sentinel) return; + if (sentinel_) |sentinel| if (byte == sentinel) return count; try output.writeByte(byte); }, } } + + return count; } pub fn encode(input: *std.Io.Reader, writer: *std.Io.Writer) EncodeErrors!void { @@ -98,6 +108,7 @@ pub fn encode(input: *std.Io.Reader, writer: *std.Io.Writer) EncodeErrors!void { // macOS before OS X would be .cr, but that is not supported by Zig } } + pub fn encodeZ(input: *std.Io.Reader, writer: *std.Io.Writer) EncodeErrors!void { switch (builtin.os.tag) { .windows => try _encode(input, writer, .crlf), @@ -135,7 +146,7 @@ fn _encode(input: *std.Io.Reader, output: *std.Io.Writer, comptime linesep: Line }, }; - while (true) { + outer: while (true) { inline for (replacements) |item| replacement: { const needle, const replacement = item; const data = input.peek(needle.len) catch |err| switch (err) { @@ -145,6 +156,7 @@ fn _encode(input: *std.Io.Reader, output: *std.Io.Writer, comptime linesep: Line if (std.mem.eql(u8, data, &needle)) { input.toss(needle.len); try output.writeAll(&replacement); + continue :outer; } } const byte = input.takeByte() catch |err| switch (err) { diff --git a/src/lib/opcode.zig b/src/lib/opcode.zig new file mode 100644 index 0000000..e12d926 --- /dev/null +++ b/src/lib/opcode.zig @@ -0,0 +1,70 @@ +const std = @import("std"); +const testing = std.testing; + +pub const Opcode = enum(u16) { + pub const DecodeErrors = error{ + InvalidOpcode, + PacketTooLong, + } || std.Io.Reader.Error || std.Io.Reader.TakeEnumError; + + rrq = 1, + wrq = 2, + data = 3, + ack = 4, + err = 5, + oack = 6, + + pub fn decode(input: *std.Io.Reader) Opcode.DecodeErrors!Opcode { + return input.takeEnum(Opcode, .big) catch |err| switch (err) { + error.InvalidEnumTag => return error.InvalidOpcode, + else => |e| return e, + }; + } + + pub fn encode(self: Opcode, output: *std.Io.Writer) !void { + try output.writeInt(u16, @intFromEnum(self), .big); + } + + test "opcode" { + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x00 }); + try testing.expectError(error.InvalidOpcode, Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x01 }); + try testing.expectEqual(.rrq, try Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x02 }); + try testing.expectEqual(.wrq, try Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x03 }); + try testing.expectEqual(.data, try Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x04 }); + try testing.expectEqual(.ack, try Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x05 }); + try testing.expectEqual(.err, try Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x06 }); + try testing.expectEqual(.oack, try Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + { + var input: std.Io.Reader = .fixed(&[_]u8{ 0x01, 0x01 }); + try testing.expectError(error.InvalidOpcode, Opcode.decode(&input)); + try testing.expectEqual(0, input.bufferedLen()); + } + } +}; diff --git a/src/lib/packet.zig b/src/lib/packet.zig index fa1333d..572bfea 100644 --- a/src/lib/packet.zig +++ b/src/lib/packet.zig @@ -5,215 +5,27 @@ const testing = std.testing; const netascii = @import("netascii.zig"); -pub const Option = enum { - blksize, - timeout, - tsize, - - pub fn decode(data: []const u8) ?Option { - inline for (@typeInfo(Option).@"enum".fields) |field| - if (std.ascii.eqlIgnoreCase(field.name, data)) - return @field(Option, field.name); - return null; - } - - test "option 1" { - try testing.expectEqual(.blksize, decode("blksize")); - try testing.expectEqual(.blksize, decode("BLKSIZE")); - try testing.expectEqual(.blksize, decode("Blksize")); - try testing.expectEqual(.blksize, decode("BlkSize")); - } - - test "option 2" { - try testing.expectEqual(.timeout, decode("timeout")); - try testing.expectEqual(.timeout, decode("TIMEOUT")); - try testing.expectEqual(.timeout, decode("Timeout")); - try testing.expectEqual(.timeout, decode("TimeOut")); - } - - test "option 3" { - try testing.expectEqual(.tsize, decode("tsize")); - try testing.expectEqual(.tsize, decode("TSIZE")); - try testing.expectEqual(.tsize, decode("Tsize")); - try testing.expectEqual(.tsize, decode("TSize")); - } - - test "option 4" { - try testing.expect(decode("apple") == null); - } -}; - -pub const Mode = enum { - pub const DecodeErrors = error{ - InvalidMode, - } || netascii.DecodeSentinelErrors; - - mail, - netascii, - octet, - - pub fn decodeZ(alloc: Allocator, input: *std.Io.Reader) DecodeErrors!Mode { - var buf: std.Io.Writer.Allocating = .init(alloc); - defer buf.deinit(); - const output = &buf.writer; - try netascii.decodeZ(input, output); - - inline for (@typeInfo(Mode).@"enum".fields) |field| - if (std.ascii.eqlIgnoreCase(field.name, buf.written())) - return @field(Mode, field.name); - - return error.InvalidMode; - } - - test "mode 1" { - const alloc = testing.allocator; - { - var input: std.io.Reader = .fixed("mail\x00"); - try testing.expectEqual(.mail, decodeZ(alloc, &input)); - } - { - var input: std.Io.Reader = .fixed("MAIL\x00"); - try testing.expectEqual(.mail, decodeZ(alloc, &input)); - } - { - var input: std.io.Reader = .fixed("Mail\x00"); - try testing.expectEqual(.mail, decodeZ(alloc, &input)); - } - { - var input: std.io.Reader = .fixed("MaIl\x00"); - try testing.expectEqual(.mail, decodeZ(alloc, &input)); - } - } - - test "mode 2" { - const alloc = testing.allocator; - { - var input: std.io.Reader = .fixed("netascii\x00"); - try testing.expectEqual(.netascii, decodeZ(alloc, &input)); - } - } - - test "mode 3" { - const alloc = testing.allocator; - { - var input: std.io.Reader = .fixed("octet\x00"); - try testing.expectEqual(.octet, decodeZ(alloc, &input)); - } - } - - test "mode 4" { - const alloc = testing.allocator; - { - var input: std.io.Reader = .fixed("apple\x00"); - try testing.expectError(error.InvalidMode, decodeZ(alloc, &input)); - } - } -}; - -pub const Opcode = enum(u16) { - pub const DecodeErrors = error{ - InvalidOpcode, - PacketTooLong, - } || std.Io.Reader.Error || std.Io.Reader.TakeEnumError; - - rrq = 1, - wrq = 2, - data = 3, - ack = 4, - err = 5, - oack = 6, - - pub fn decode(input: *std.Io.Reader) DecodeErrors!Opcode { - return input.takeEnum(Opcode, .big) catch |err| switch (err) { - error.InvalidEnumTag => return error.InvalidOpcode, - else => |e| return e, - }; - } - - test "opcode" { - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x00 }); - try testing.expectError(error.InvalidOpcode, decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x01 }); - try testing.expectEqual(.rrq, try decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x02 }); - try testing.expectEqual(.wrq, try decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x03 }); - try testing.expectEqual(.data, try decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x04 }); - try testing.expectEqual(.ack, try decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x05 }); - try testing.expectEqual(.err, try decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x00, 0x06 }); - try testing.expectEqual(.oack, try decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - { - var input: std.Io.Reader = .fixed(&[_]u8{ 0x01, 0x01 }); - try testing.expectError(error.InvalidOpcode, decode(&input)); - try testing.expectEqual(0, input.bufferedLen()); - } - } -}; - -pub const ErrorCode = enum(u16) { - not_defined = 0, - file_not_found = 1, - access_violation = 2, - disk_full = 3, - illegal_operation = 4, - tid_unknown = 5, - file_exists = 6, - no_such_user = 7, - term_option = 8, - _, - - pub fn decode(input: *std.Io.Reader) !ErrorCode { - return try input.takeEnum(ErrorCode, .big); - } - - // pub fn message(self: @This()) []const u8 { - // return switch (self) { - // .not_defined => "", - // .file_not_found => "File not found", - // .access_violation => "Access violation", - // .disk_full => "Disk full or allocation exceeded", - // .illegal_operation => "Illegal TFTP operation", - // .tid_unknown => "Unknown transfer ID", - // .file_exists => "File already exists", - // .no_such_user => "No such user", - // .term_option => "Terminate transfer due to option negotiation", - // }; - // } -}; - fn decodeFilenameZ(alloc: Allocator, input: *std.Io.Reader) (Allocator.Error || netascii.DecodeSentinelErrors)![]const u8 { var buf: std.Io.Writer.Allocating = .init(alloc); errdefer buf.deinit(); const output = &buf.writer; - try netascii.decodeZ(input, output); + _ = try netascii.decodeZ(input, output); return buf.toOwnedSlice(); } +fn encodeFilenameZ(filename: []const u8, output: *std.Io.Writer) !void { + var input: std.Io.Reader = .fixed(filename); + try netascii.encodeZ(&input, output); +} + pub const Packet = union(Opcode) { + pub const BUFFER_LENGTH = std.math.maxInt(u16); + pub const Buf = [BUFFER_LENGTH]u8; + pub const Options = @import("Options.zig"); + pub const Mode = @import("mode.zig").Mode; + pub const Opcode = @import("opcode.zig").Opcode; + pub const ErrorCode = @import("errorcode.zig").ErrorCode; + pub const DecodeErrors = error{ UnterminatedFilename, UnterminatedMode, @@ -223,8 +35,8 @@ pub const Packet = union(Opcode) { pub const DecodeOptions = struct { mode: ?Mode = null, blocksize: ?u16 = null, - timeout: ?u8 = null, - transfer_size: ?u64 = null, + // timeout: ?u8 = null, + // transfer_size: ?u64 = null, pub const default: @This() = .{}; pub const netascii: @This() = .{ .mode = .netascii }; @@ -232,127 +44,86 @@ pub const Packet = union(Opcode) { pub const octet: @This() = .{ .mode = .octet }; }; - rrq: struct { + rrq: Rrq, + wrq: Wrq, + data: Data, + ack: Ack, + err: Err, + oack: Oack, + + pub const Rrq = struct { filename: []const u8, mode: Mode, - blocksize: ?u16 = null, - timeout: ?u8 = null, - transfer_size: ?u64 = null, - }, + options: Options, + }; - wrq: struct { + pub const Wrq = struct { filename: []const u8, mode: Mode, - blocksize: ?u16 = null, - timeout: ?u8 = null, - transfer_size: ?u64 = null, - }, + options: Options, + }; - data: struct { + pub const Data = struct { block: u16, mode: Mode, - buffer: []const u8, data: []const u8, last: bool = false, - }, + }; - ack: struct { + pub const Ack = struct { block: u16, - }, + }; - err: struct { + pub const Err = struct { code: ErrorCode, - msg: []const u8, - }, + msg: ?[]const u8 = null, + }; - oack: struct { - blocksize: ?u16 = null, - timeout: ?u8 = null, - transfer_size: ?u64 = null, - }, + pub const Oack = struct { + options: Options, + }; - pub fn decode(alloc: Allocator, data: []const u8, options: DecodeOptions) DecodeErrors!*Packet { - var input: std.Io.Reader = .fixed(data); + pub fn opcode(self: *const Packet) Opcode { + return switch (self.*) { + .rrq => .rrq, + .wrq => .wrq, + .data => .data, + .ack => .ack, + .err => .err, + .oack => .oack, + }; + } - const opcode: Opcode = try .decode(&input); + pub fn decode(data: []const u8, buf: []u8, decode_options: DecodeOptions) DecodeErrors!Packet { + var input: std.Io.Reader = .fixed(data); + var fba: std.heap.FixedBufferAllocator = .init(buf); + const alloc = fba.allocator(); - switch (opcode) { - .rrq, .wrq => { + switch (try Opcode.decode(&input)) { + .rrq, .wrq => |code| { const filename = try decodeFilenameZ(alloc, &input); - errdefer alloc.free(filename); - - const mode: Mode = try .decodeZ(alloc, &input); - - var blocksize: ?u16 = null; - var timeout: ?u8 = null; - var transfer_size: ?u64 = null; - - options: while (true) { - const option_str = input.peekDelimiterExclusive(0) catch |err| switch (err) { - error.StreamTooLong => break :options, - error.EndOfStream => break :options, - else => |e| return e, - }; - input.toss(option_str.len + 1); - const value_str = input.peekDelimiterExclusive(0) catch |err| switch (err) { - error.StreamTooLong => break :options, - error.EndOfStream => break :options, - else => |e| return e, - }; - input.toss(value_str.len + 1); - const option = Option.decode(option_str) orelse continue :options; - switch (option) { - .blksize => { - const value = std.fmt.parseUnsigned(u16, value_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => continue :options, - }; - if (value < 8 or value > 65464) continue :options; - blocksize = value; - }, - .timeout => { - const value = std.fmt.parseUnsigned(u8, value_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => continue :options, - }; - if (value < 1) continue :options; - timeout = value; - }, - .tsize => { - const value = std.fmt.parseUnsigned(u64, value_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => continue :options, - }; - transfer_size = value; - }, - } - } + const mode: Mode = try .decodeZ(&input); + const options: Options = try .decode(&input); - switch (opcode) { + switch (code) { .rrq => { - const packet = try alloc.create(Packet); - - packet.* = .{ + return .{ .rrq = .{ .filename = filename, .mode = mode, - .blocksize = blocksize, - .timeout = timeout, - .transfer_size = transfer_size, + .options = options, }, }; - return packet; }, .wrq => { - const packet = try alloc.create(Packet); - packet.* = .{ + return .{ .wrq = .{ .filename = filename, .mode = mode, - .blocksize = blocksize, - .timeout = timeout, - .transfer_size = transfer_size, + .options = options, }, }; - return packet; }, .data, @@ -365,157 +136,102 @@ pub const Packet = union(Opcode) { .data => { const block = try input.takeInt(u16, .big); - const mode = options.mode orelse return error.ModeRequired; - const blocksize: u16 = options.blocksize orelse 512; + const mode = decode_options.mode orelse return error.ModeRequired; + const blocksize: u16 = decode_options.blocksize orelse 512; - const buffer = try alloc.alloc(u8, blocksize); - errdefer alloc.free(buffer); + var buffer: std.Io.Writer.Allocating = .init(alloc); + const output = &buffer.writer; - var output: std.Io.Writer = .fixed(buffer); + // const len = switch (mode) { + // .mail, .netascii => try netascii.decode(&input, output), + // .octet => try input.streamRemaining(output), + // }; + const len = try input.streamRemaining(output); - switch (mode) { - .mail, .netascii => { - try netascii.decode(&input, &output); - }, - .octet => { - _ = try input.streamRemaining(&output); - }, - } - - const packet = try alloc.create(Packet); - errdefer alloc.destroy(packet); - - packet.* = .{ + return .{ .data = .{ .block = block, .mode = mode, - .buffer = buffer, - .data = output.buffered(), - .last = output.end < blocksize, + .data = buffer.writter(), + .last = len < blocksize, }, }; - - return packet; }, .ack => { const block = try input.takeInt(u16, .big); - const packet = try alloc.create(Packet); - errdefer alloc.destroy(packet); - - packet.* = .{ + return .{ .ack = .{ .block = block, }, }; - - return packet; }, .err => { const code: ErrorCode = try .decode(&input); - var buf: std.Io.Writer.Allocating = .init(alloc); - const output = &buf.writer; - try netascii.decodeZ(&input, output); - - const packet = try alloc.create(Packet); - errdefer alloc.destroy(packet); + var msgbuf: std.Io.Writer.Allocating = .init(alloc); + const output = &msgbuf.writer; + _ = try netascii.decodeZ(&input, output); - packet.* = .{ + return .{ .err = .{ .code = code, - .msg = try buf.toOwnedSlice(), + .msg = try msgbuf.toOwnedSlice(), }, }; }, .oack => { - var blocksize: ?u16 = null; - var timeout: ?u8 = null; - var transfer_size: ?u64 = null; - - options: while (true) { - const option_str = input.peekDelimiterExclusive(0) catch |err| switch (err) { - error.StreamTooLong => break :options, - else => |e| return e, - }; - input.toss(option_str.len + 1); - const value_str = input.peekDelimiterExclusive(0) catch |err| switch (err) { - error.StreamTooLong => break :options, - else => |e| return e, - }; - input.toss(value_str.len + 1); - - const option = Option.decode(option_str) orelse continue :options; - switch (option) { - .blksize => { - const value = std.fmt.parseUnsigned(u16, value_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => continue :options, - }; - if (value < 8 or value > 65464) continue :options; - blocksize = value; - }, - .timeout => { - const value = std.fmt.parseUnsigned(u8, value_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => continue :options, - }; - if (value < 1) continue :options; - timeout = value; - }, - .tsize => { - const value = std.fmt.parseUnsigned(u64, value_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => continue :options, - }; - transfer_size = value; - }, - } - } - - const packet = try alloc.create(Packet); - errdefer alloc.destroy(packet); - - packet.* = .{ + return .{ .oack = .{ - .blocksize = blocksize, - .timeout = timeout, - .transfer_size = transfer_size, + .options = try .decode(&input), }, }; - - return packet; }, } unreachable; } - pub fn deinit(self: *const Packet, alloc: Allocator) void { + pub fn encode(self: *const Packet, buf: []u8) ![]const u8 { + var output: std.Io.Writer = .fixed(buf); + try self.opcode().encode(&output); + switch (self.*) { - .rrq => |v| { - alloc.free(v.filename); + inline .rrq, .wrq => |p| { + try encodeFilenameZ(p.filename, &output); + try p.mode.encodeZ(&output); + try p.options.encode(&output); + }, + .data => |p| { + try output.writeInt(u16, p.block, .big); + try output.writeAll(p.data); }, - .wrq => |v| { - alloc.free(v.filename); + .ack => |p| { + try output.writeInt(u16, p.block, .big); }, - .data => |v| { - alloc.free(v.buffer); + .err => |p| { + try p.code.encode(&output); + const msg = p.msg orelse p.code.message(); + var input: std.Io.Reader = .fixed(msg); + try netascii.encodeZ(&input, &output); }, - .ack => {}, - .err => |v| { - alloc.free(v.msg); + .oack => |p| { + try p.options.encode(&output); }, - .oack => {}, } - alloc.destroy(self); + + try output.flush(); + + return output.buffered(); } test "datagram decode 1" { - const alloc = testing.allocator; + var buf: Packet.Buf = undefined; const input = &[_]u8{ 0x0, 0x1, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x74, 0x78, 0x74, 0x0, 0x6e, 0x65, 0x74, 0x61, 0x73, 0x63, 0x69, 0x69, 0x0 }; - const packet: *Packet = try .decode(alloc, input, .default); - defer packet.deinit(alloc); - try testing.expect(packet.* == .rrq); + const packet: Packet = try .decode(input, &buf, .default); + try testing.expect(packet == .rrq); try testing.expectEqualStrings("test.txt", packet.rrq.filename); try testing.expectEqual(.netascii, packet.rrq.mode); } diff --git a/src/main.zig b/src/main.zig index 02f9c97..13655d5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,48 +1,42 @@ const std = @import("std"); -const builtin = @import("builtin"); const jtftp = @import("jtftp"); -const nl = @import("netlink"); -const link = @import("link.zig"); -var debug_allocator: std.heap.DebugAllocator(.{}) = .init; +pub fn main(init: std.process.Init) !void { + const io = init.io; + const alloc = init.gpa; -pub fn main() !void { - const alloc, const is_debug = allocator: { - break :allocator switch (builtin.mode) { - .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true }, - .ReleaseFast, .ReleaseSmall => .{ std.heap.smp_allocator, false }, - }; - }; - defer if (is_debug) { - _ = debug_allocator.deinit(); - }; + const addr = try std.Io.net.IpAddress.parseLiteral("127.0.0.1:6969"); + const socket = try addr.bind(io, .{ .mode = .dgram, .protocol = .udp }); - try link.list(); + var group: std.Io.Group = .init; + defer group.cancel(io); - const socket = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0); - - const local_addr = try std.net.Address.parseIp("127.0.0.1", 6969); - try std.posix.bind(socket, &local_addr.any, local_addr.getOsSockLen()); - - var buffer: [std.math.maxInt(u16)]u8 = undefined; while (true) { - var remote_addr: std.net.Address = undefined; - var remote_addr_len: std.posix.socklen_t = @sizeOf(std.net.Address); - const len = try std.posix.recvfrom(socket, &buffer, 0, &remote_addr.any, &remote_addr_len); - - const data = buffer[0..len]; - var wbuf: [64]u8 = undefined; - var wr = std.Io.Writer.fixed(&wbuf); - try wr.print("{f}", .{remote_addr}); - std.debug.print("received {d} bytes from {s}\n", .{ len, wr.buffered() }); - - const packet = jtftp.Packet.decode(alloc, data, .default) catch |err| { - std.debug.print("problem decoding packet: {t}\n", .{err}); - continue; - }; - std.debug.print("{t}\n", .{packet.*}); - // switch (packet) { - // .rrq => handleRead(), - // } + var messages: [5]std.Io.net.IncomingMessage = @splat(.init); + var buffer: [5 * jtftp.Packet.BUFFER_LENGTH]u8 = undefined; + const err_, const count = socket.receiveManyTimeout(io, &messages, &buffer, .{}, .{ + .duration = .{ + .clock = .real, + .raw = .fromMilliseconds(250), + }, + }); + + for (messages[0..count]) |message| { + const data = try alloc.dupe(u8, message.data); + try group.concurrent(io, jtftp.handleIncomingMessage, .{ + io, + alloc, + addr, + message.from, + data, + }); + } + + if (err_) |err| { + switch (err) { + error.Timeout => {}, + else => return err, + } + } } } diff --git a/src/root.zig b/src/root.zig index 82268fa..0366004 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,10 +1,171 @@ //! By convention, root.zig is the root source file when making a library. const std = @import("std"); -const packet = @import("lib/packet.zig"); -pub const Packet = packet.Packet; -pub const netascii = @import("lib/netascii.zig"); +const log = std.log.scoped(.jtftp); + +pub const Packet = @import("lib/packet.zig").Packet; +const netascii = @import("lib/netascii.zig"); + +pub fn handleIncomingMessage( + io: std.Io, + alloc: std.mem.Allocator, + to: std.Io.net.IpAddress, + remote: std.Io.net.IpAddress, + data: []const u8, +) error{Canceled}!void { + _handleIncomingMessage(io, alloc, to, remote, data) catch |err| switch (err) { + // error.Canceled => return error.Canceled, + else => |e| { + log.warn("handle incoming message failed with {t}", .{e}); + }, + }; +} + +fn _handleIncomingMessage( + io: std.Io, + alloc: std.mem.Allocator, + to: std.Io.net.IpAddress, + remote: std.Io.net.IpAddress, + initial_data: []const u8, +) !void { + defer alloc.free(initial_data); + var local = to; + local.setPort(0); + const socket = try local.bind(io, .{ .mode = .dgram, .protocol = .udp }); + + var input_buf: [std.math.maxInt(u16)]u8 = undefined; + const initial: Packet = try .decode(initial_data, &input_buf, .{}); + + log.info("initial {t} packet from {f}", .{ initial, remote }); + + switch (initial) { + .wrq => |*wrq| { + log.info("wrq: {s}", .{wrq.filename}); + + try handleWrite(io, alloc, socket, &remote, wrq); + }, + .rrq => |*rrq| { + log.info("rrq: {s}", .{rrq.filename}); + const packet: Packet = .{ + .err = .{ + .code = .access_violation, + }, + }; + var buf: [std.math.maxInt(u16)]u8 = undefined; + const d = try packet.encode(&buf); + try socket.send(io, &remote, d); + return; + }, + else => { + const packet: Packet = .{ + .err = .{ + .code = .illegal_operation, + }, + }; + var output_buf: [std.math.maxInt(u16)]u8 = undefined; + const d = try packet.encode(&output_buf); + try socket.send(io, &remote, d); + return; + }, + } +} + +fn handleWrite( + io: std.Io, + alloc: std.mem.Allocator, + socket: std.Io.net.Socket, + remote: *const std.Io.net.IpAddress, + wrq: *const Packet.Wrq, +) !void { + var acc: std.Io.Writer.Allocating = .init(alloc); + var writer = &acc.writer; + + var block: u16 = 0; + + const total_timeout = (@as(i96, @min(1, wrq.options.timeout orelse 30))) * std.time.ns_per_s; + const packet_timeout: std.Io.Timeout = .{ + .duration = .{ + .clock = .real, + .raw = .fromNanoseconds(@divFloor(total_timeout, 5)), + }, + }; + var last_received: std.Io.Timestamp = .now(io, .real); + + while (true) { + { + var output_buf: Packet.Buf = undefined; + const packet: Packet = packet: { + if (block == 0) { + if (wrq.options.empty()) break :packet .{ + .ack = .{ + .block = 0, + }, + }; + break :packet .{ + .oack = .{ + .options = wrq.options, + }, + }; + } + break :packet .{ + .ack = .{ + .block = block, + }, + }; + }; + + try socket.send(io, remote, try packet.encode(&output_buf)); + } + + var input_message_buf: Packet.Buf = undefined; + const incoming_message = socket.receiveTimeout(io, &input_message_buf, packet_timeout) catch |err| switch (err) { + error.Timeout => { + const delta = last_received.untilNow(io, .real); + if (delta.toNanoseconds() > total_timeout) return error.Timeout; + continue; + }, + else => |e| return e, + }; + + if (!remote.eql(&incoming_message.from)) return error.PacketFromInvalidSource; + + var input_packet_buf: Packet.Buf = undefined; + const input_packet: Packet = try .decode(incoming_message.data, &input_packet_buf, .{ + .mode = wrq.mode, + .blocksize = wrq.options.blocksize, + }); + + switch (input_packet) { + .data => |data| { + last_received = .now(io, .real); + block = data.block; + log.warn("received {d} bytes, last: {}", .{ data.data.len, data.last }); + try writer.writeAll(data.data); + if (data.last) { + log.info("last packet received", .{}); + const packet: Packet = .{ + .ack = .{ + .block = block, + }, + }; + var output_buf: [std.math.maxInt(u16)]u8 = undefined; + try socket.send(io, remote, try packet.encode(&output_buf)); + return; + } + }, + .rrq, + .wrq, + .ack, + .err, + .oack, + => { + log.err("illegal packet type", .{}); + return error.IllegalPacketType; + }, + } + } +} test { - _ = std.testing.refAllDeclsRecursive(@This()); + _ = std.testing.refAllDecls(@This()); } diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b281150 --- /dev/null +++ b/uv.lock @@ -0,0 +1,70 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, +] + +[[package]] +name = "jtftp" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "arrow" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = "~=24.1.0" }, + { name = "arrow", specifier = "~=1.3.0" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20260508" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/9b/ee1674cbe9ec50bb824f35a5dc0ce5fd1f1b6196ba1213e3fe6f33b4ce32/types_python_dateutil-2.9.0.20260508.tar.gz", hash = "sha256:596a6d63d81f587bf04c8254fb78df9d2344e915ce67948d7400512e3a6206d5", size = 17033, upload-time = "2026-05-08T04:47:08.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/7c/1788ff10edf56031d74ad3fba40947e0e8b82e3a529b30e6b7d71c191dec/types_python_dateutil-2.9.0.20260508-py3-none-any.whl", hash = "sha256:bfc6fd2d81aa86e5ac97206a64304f6bd247426eedbca9b98619bbc48c6a1c10", size = 18425, upload-time = "2026-05-08T04:47:07.207Z" }, +]