├── .gitignore ├── src ├── log.zig ├── main.zig ├── config.zig ├── protocol.zig ├── repl.zig ├── client.zig ├── test.zig └── server.zig └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /zig-*/ 2 | /local 3 | /testdir -------------------------------------------------------------------------------- /src/log.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn printFmt(comptime fmt: []const u8, args: anytype) void { 4 | const writer = std.io.getStdOut().writer(); 5 | std.fmt.format(writer, fmt, args) catch {}; 6 | } 7 | 8 | pub fn errPrintFmt(comptime fmt: []const u8, args: anytype) void { 9 | const writer = std.io.getStdErr().writer(); 10 | std.fmt.format(writer, fmt, args) catch {}; 11 | } 12 | 13 | pub fn print(text: []const u8) void { 14 | std.io.getStdOut().writeAll(text) catch {}; 15 | } 16 | 17 | pub fn println(text: []const u8) void { 18 | printFmt("{s}\n", .{text}); 19 | } 20 | 21 | pub fn eprint(text: []const u8) void { 22 | std.io.getStdErr().writeAll(text) catch {}; 23 | } 24 | 25 | pub fn eprintln(text: []const u8) void { 26 | errPrintFmt("{s}\n", .{text}); 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qooil 2 | 3 | A file transfer utility written in Zig. 4 | 5 | ## Running 6 | 7 | The server and client reside in the same binary. run `qooil -h` for help: 8 | 9 | ``` 10 | Qooil - An FTP-like file transportation utility 11 | -s run server 12 | -c run client (default) 13 | -a host/address to bind/connect 14 | -p port to listen/connect (default is 7070) 15 | -h show this help 16 | -j server thread count 17 | 18 | Examples: 19 | 20 | qooil 21 | # connect to server running on localhost on port 7070 22 | 23 | qooil -s -p 7777 -a 127.0.0.1 -j 100 24 | # run server on port 7777 and loopback interface 25 | # with thread pool size of 100 threads 26 | ``` 27 | 28 | ## Repl Commands 29 | 30 | After connecting to the server, the client gives you a REPL to communicate to the server: 31 | 32 | ``` 33 | > help 34 | 35 | cat cat | print file content to terminal 36 | cd cd | change CWD to dir 37 | delete delete | delete file 38 | get get | download file from server 39 | help print this help 40 | ls ls [dir] | shows entries in CWD or dir 41 | ping check whether server is up or not 42 | put put | upload file to server 43 | pwd show CWD 44 | quit close connection 45 | 46 | ``` 47 | 48 | ## Todo 49 | - [ ] directory manipulation 50 | - [ ] client download manager & download history 51 | - [ ] parallel transfers 52 | - [ ] multi-root 53 | - [ ] proxy/mirror 54 | - [ ] authentication 55 | - [ ] per-dir/file permissions 56 | - [ ] compression 57 | - [ ] encryption -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | const net = std.net; 4 | const protocol = @import("protocol.zig"); 5 | const config_mod = @import("config.zig"); 6 | const log = @import("log.zig"); 7 | const server_mod = @import("server.zig"); 8 | const Repl = @import("repl.zig"); 9 | const tests = @import("test.zig"); 10 | 11 | const Server = server_mod.Server; 12 | const Message = protocol.Message; 13 | const Header = protocol.Header; 14 | const ServerErrors = protocol.ServerErrors; 15 | const ArgParser = config_mod.ArgParser; 16 | const Config = config_mod.Config; 17 | 18 | var gpa: std.heap.GeneralPurposeAllocator(.{}) = undefined; 19 | 20 | const HELP_MESSAGE = 21 | \\Qooil - An FTP-like file transportation utility 22 | \\ -s run server 23 | \\ -c run client (default) 24 | \\ -a host/address to bind/connect 25 | \\ -p port to listen/connect (default is 7070) 26 | \\ -h show this help 27 | \\ -j server thread count 28 | \\ 29 | \\Examples: 30 | \\ 31 | \\ qooil 32 | \\ # connect to server running on localhost on port 7070 33 | \\ 34 | \\ qooil -s -p 7777 -a 127.0.0.1 -j 100 35 | \\ # run server on port 7777 and loopback interface 36 | \\ # with thread pool size of 100 threads 37 | \\ 38 | ; 39 | 40 | fn loadConfig() !Config { 41 | gpa = .{}; 42 | var parser = ArgParser.init(std.process.args()); 43 | const args = parser.parse() catch { 44 | log.eprint(HELP_MESSAGE); 45 | std.process.exit(1); 46 | }; 47 | if (args.help) { 48 | log.print(HELP_MESSAGE); 49 | std.process.exit(0); 50 | } 51 | var conf = Config.init(gpa.allocator()); 52 | try conf.parseCLI(args); 53 | return conf; 54 | } 55 | 56 | pub fn main() !void { 57 | const conf = try loadConfig(); 58 | if (conf.is_server) { 59 | var server = Server.init(conf); 60 | try server.runServer(); 61 | } else { 62 | var repl = Repl.init(conf); 63 | try repl.mainloop(conf); 64 | } 65 | } 66 | 67 | test { 68 | std.testing.refAllDecls(tests); 69 | } 70 | -------------------------------------------------------------------------------- /src/config.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | 4 | fn ArrayListFromIterator( 5 | comptime T: type, 6 | iter: anytype, 7 | allocator: std.mem.Allocator, 8 | ) !std.ArrayList(T) { 9 | var list = std.ArrayList(T).init(allocator); 10 | var iter_mut = iter; 11 | while (iter_mut.next()) |n| { 12 | try list.append(n); 13 | } 14 | return list; 15 | } 16 | 17 | const ExecMode = enum { 18 | Client, 19 | Server, 20 | }; 21 | 22 | const Arguments = struct { 23 | mode: ?ExecMode, 24 | address: ?[]const u8, 25 | port: ?[]const u8, 26 | help: bool, 27 | thread_count: ?[]const u8, 28 | }; 29 | 30 | const ConfigError = error{ 31 | MissingOption, 32 | InvalidPort, 33 | InvalidThreadCount, 34 | UnknownFlag, 35 | }; 36 | 37 | pub const ArgParser = struct { 38 | const Self = @This(); 39 | 40 | current: ?[:0]const u8, 41 | iterator: std.process.ArgIterator, 42 | arguments: Arguments, 43 | fault: ?[]const u8, 44 | 45 | pub fn init(iterator: std.process.ArgIterator) Self { 46 | var iter_mut = iterator; 47 | _ = iter_mut.next(); 48 | return .{ 49 | .iterator = iter_mut, 50 | .arguments = .{ 51 | .thread_count = null, 52 | .port = null, 53 | .mode = null, 54 | .address = null, 55 | .help = false, 56 | }, 57 | .fault = null, 58 | .current = null, 59 | }; 60 | } 61 | 62 | fn read(self: *Self) ?[:0]const u8 { 63 | return self.iterator.next(); 64 | } 65 | 66 | fn peek(self: *Self) ?[:0]const u8 { 67 | if (self.current) |_| {} else { 68 | self.current = self.read(); 69 | } 70 | return self.current; 71 | } 72 | 73 | fn next(self: *Self) ?[:0]const u8 { 74 | if (self.current) |token| { 75 | self.current = null; 76 | return token; 77 | } 78 | return self.read(); 79 | } 80 | 81 | fn isFlag(self: *Self) bool { 82 | const token = self.peek(); 83 | if (token) |tkn| { 84 | return tkn.len == 2 and tkn[0] == '-'; 85 | } 86 | return false; 87 | } 88 | 89 | fn expect(self: *Self, name: []const u8) ![:0]const u8 { 90 | if (self.next()) |token| { 91 | return token; 92 | } else { 93 | self.fault = name; 94 | return ConfigError.MissingOption; 95 | } 96 | } 97 | 98 | fn nextFlag(self: *Self) !void { 99 | const token = self.next() orelse unreachable; 100 | const flag = token[1]; 101 | switch (flag) { 102 | 'c' => { 103 | self.arguments.mode = ExecMode.Client; 104 | }, 105 | 'h' => { 106 | self.arguments.help = true; 107 | }, 108 | 's' => { 109 | self.arguments.mode = ExecMode.Server; 110 | }, 111 | 'a' => { 112 | self.arguments.address = try self.expect("address"); 113 | }, 114 | 'p' => { 115 | self.arguments.port = try self.expect("port"); 116 | }, 117 | 'j' => { 118 | self.arguments.thread_count = try self.expect("thread count"); 119 | }, 120 | else => { 121 | self.fault = token; 122 | return ConfigError.UnknownFlag; 123 | }, 124 | } 125 | } 126 | 127 | fn nextPositional(self: *Self, name: []const u8) ![:0]const u8 { 128 | while (self.isFlag()) { 129 | try self.nextFlag(); 130 | } 131 | if (self.next()) |value| { 132 | return value; 133 | } else { 134 | self.fault = name; 135 | return ConfigError.MissingOption; 136 | } 137 | } 138 | 139 | fn parseAllFlags(self: *Self) !void { 140 | while (self.peek()) |_| { 141 | if (self.isFlag()) { 142 | try self.nextFlag(); 143 | } else { 144 | _ = self.next(); 145 | } 146 | } 147 | } 148 | 149 | pub fn parse(self: *Self) !Arguments { 150 | try self.parseAllFlags(); 151 | return self.arguments; 152 | } 153 | }; 154 | 155 | const DEFAULT_PORT = 7070; 156 | 157 | pub const Config = struct { 158 | const Self = @This(); 159 | 160 | address: []const u8, 161 | port: u16, 162 | is_server: bool, 163 | allocator: std.mem.Allocator, 164 | thread_pool_size: u32, 165 | 166 | fn parsePort(self: *Self, args: Arguments) !void { 167 | if (args.port) |p| { 168 | self.port = switch (std.zig.parseNumberLiteral(p)) { 169 | .failure, .float, .big_int => return ConfigError.InvalidPort, 170 | .int => |num| blk: { 171 | if (num < 1 or num > 0xffff) { 172 | return ConfigError.InvalidPort; 173 | } else { 174 | break :blk @intCast(num); 175 | } 176 | }, 177 | }; 178 | } 179 | } 180 | 181 | fn parseAddress(self: *Self, args: Arguments) !void { 182 | if (args.address) |addr| { 183 | if (net.isValidHostName(addr)) { 184 | self.address = addr; 185 | } 186 | } 187 | } 188 | 189 | fn parseThreadCount(self: *Self, args: Arguments) !void { 190 | if (args.thread_count) |tc| { 191 | const count = std.fmt.parseInt(u32, tc, 10) catch { 192 | return error.InvalidThreadCount; 193 | }; 194 | if (count == 0) { 195 | return error.InvalidThreadCount; 196 | } 197 | self.thread_pool_size = count; 198 | } 199 | } 200 | 201 | pub fn parseCLI(self: *Self, args: Arguments) !void { 202 | try self.parsePort(args); 203 | try self.parseAddress(args); 204 | try self.parseThreadCount(args); 205 | if (args.mode) |m| { 206 | self.is_server = m == ExecMode.Server; 207 | } 208 | } 209 | 210 | pub fn init(allocator: std.mem.Allocator) Self { 211 | return .{ 212 | .allocator = allocator, 213 | .is_server = false, 214 | .address = "0.0.0.0", 215 | .port = DEFAULT_PORT, 216 | .thread_pool_size = 200, 217 | }; 218 | } 219 | }; 220 | -------------------------------------------------------------------------------- /src/protocol.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | /// **The Protocol** 3 | /// 4 | /// The communication between the server and client starts by the client opening 5 | /// a TCP connection to the server and terminates by either of the peers closing 6 | /// the connection or the client sending the `quit` message. 7 | /// Multiple messages can be sent in a single TCP connection. For each message 8 | /// that the client sends the server MUST send back the `Error` message, or the 9 | /// corresponding message types as a response, with that being just a simple `OK` 10 | /// message, or even multiple messages each one carrying a payload. 11 | /// 12 | /// **General format** 13 | /// 14 | /// The format of a message, which is the same for both server and client is 15 | /// as follows. The sender of the message writes these sections of the message 16 | /// on the stream (the TCP connection) sequentially form left to right: 17 | /// 18 | /// `[header: fixed size per tag][payload: variable]` 19 | /// 20 | /// The tag is a two-byte little-endian word which indicates the message type. 21 | /// Each type of message (that we in this document refer to as `type`, e.g. 22 | /// `List` for the List message) has it's own header. 23 | /// The message might not have a payload, e.g. `Error` which only has a header 24 | /// or it might just be a sinletag with no header and payload, e.g. `Ping` or `Pwd` 25 | /// The size of the header is fixed per the tag, meaning any `File` message header 26 | /// has a fixed size of `@sizeOf(FileHeader)`. 27 | /// All numbers must be litte-endian. 28 | /// 29 | /// **Sending payloads** 30 | /// 31 | /// The convention for sending payload which has a variable size is to specify the 32 | /// length in the header of the message (look as `CdHeader`). If the payload 33 | /// consists of two variable-size data (like file path & file content) then both 34 | /// pathes must be mentioned in the header and the data. 35 | /// In case of having to send may variable-size elements (like an array of strings) 36 | /// the convention is that the sender should send messages sequentially each 37 | /// containing a single payload, and then sending the `end` message to indicate the 38 | /// end of elements 39 | pub const Message = struct { 40 | header: Header, 41 | }; 42 | 43 | const TagType = u16; 44 | 45 | /// All headers must be packed structs {} as it's standardized 46 | /// by the language that packed structs have a guaranteed layout. 47 | /// Some of these messages are only expected to be sent from the 48 | /// client and others are responses sent from the server. 49 | pub const Header = union(enum(TagType)) { 50 | Read: ReadHeader = 1, 51 | File: FileHeader = 2, 52 | List: ListHeader = 3, 53 | Entry: EntryHeader = 4, 54 | /// indictaes that the last element has been sent 55 | End: EmptyHeader = 5, 56 | Cd: CdHeader = 6, 57 | Pwd: EmptyHeader = 7, 58 | Path: PathHeader = 8, 59 | /// sent by the server as a success response. 60 | Ok: EmptyHeader = 9, 61 | GetInfo: EmptyHeader = 10, 62 | /// parameter negotiation message 63 | Info: InfoHeader = 11, 64 | Ping: EmptyHeader = 12, 65 | PingReply: EmptyHeader = 13, 66 | /// can be sent from the client to terminate the connection 67 | Quit: EmptyHeader = 14, 68 | QuitReply: EmptyHeader = 15, 69 | Write: WriteHeader = 16, 70 | Delete: DeleteHeader = 17, 71 | /// This is only returned by the message parser 72 | /// to indicate that the peer has returned a 73 | /// message with an unknown type tag. This message 74 | /// type should not be sent by neither the client 75 | /// nor the server. 76 | Corrupt: CorruptHeader = 18, 77 | Error: ErrorHeader = 19, 78 | GetStat: PathHeader = 20, 79 | Stat: StatHeader = 21, 80 | }; 81 | pub const NodeType = enum(u8) { 82 | File = 1, 83 | Dir = 2, 84 | }; 85 | pub const StatHeader = struct { 86 | ty: NodeType, 87 | /// MUST always be zero for directories 88 | size: u64, 89 | }; 90 | pub const EmptyHeader = packed struct {}; 91 | pub const ReadHeader = packed struct { 92 | length: u16, 93 | }; 94 | pub const WriteHeader = packed struct { 95 | length: u16, 96 | }; 97 | pub const DeleteHeader = packed struct { 98 | length: u16, 99 | }; 100 | pub const ListHeader = packed struct { 101 | length: u16, 102 | }; 103 | pub const CdHeader = packed struct { 104 | /// length of the path payload 105 | length: u16, 106 | }; 107 | pub const FileHeader = packed struct { 108 | size: u64, 109 | }; 110 | pub const EntryHeader = packed struct { 111 | length: u8, 112 | is_dir: bool, 113 | }; 114 | pub const PathHeader = packed struct { 115 | length: u16, 116 | }; 117 | pub const CorruptHeader = packed struct { 118 | tag: TagType, 119 | }; 120 | pub const ErrorHeader = packed struct { 121 | code: u16, 122 | arg1: u32, 123 | arg2: u32, 124 | }; 125 | pub const InfoHeader = struct { 126 | max_name: usize, 127 | max_path: usize, 128 | }; 129 | 130 | /// These errors are returned by the server to indicate 131 | /// an error on the side of client. 132 | pub const ServerError = error{ 133 | UnexpectedMessage, 134 | CorruptMessageTag, 135 | InvalidFileName, 136 | UnexpectedEndOfConnection, 137 | NonExisting, 138 | IsNotFile, 139 | IsNotDir, 140 | AccessDenied, 141 | CantOpen, 142 | /// Just to indicate failure to decode error. 143 | /// must never be returned from the server. 144 | Unrecognized, 145 | }; 146 | 147 | pub fn encodeServerError(err: ServerError) u16 { 148 | return switch (err) { 149 | error.UnexpectedMessage => 1, 150 | error.CorruptMessageTag => 2, 151 | error.InvalidFileName => 3, 152 | error.UnexpectedEndOfConnection => 4, 153 | error.NonExisting => 5, 154 | error.IsNotFile => 6, 155 | error.IsNotDir => 7, 156 | error.AccessDenied => 8, 157 | error.CantOpen => 9, 158 | error.Unrecognized => 0xffff, 159 | }; 160 | } 161 | 162 | pub fn decodeServerError(code: u16) ServerError { 163 | return switch (code) { 164 | 1 => error.UnexpectedMessage, 165 | 2 => error.CorruptMessageTag, 166 | 3 => error.InvalidFileName, 167 | 4 => error.UnexpectedEndOfConnection, 168 | 5 => error.NonExisting, 169 | 6 => error.IsNotFile, 170 | 7 => error.IsNotDir, 171 | 8 => error.AccessDenied, 172 | 9 => error.CantOpen, 173 | else => error.Unrecognized, 174 | }; 175 | } 176 | 177 | /// `hdr` must be a pointer to a struct instance. 178 | /// Converts every integer field to little-endian. 179 | fn headerToLittle(hdr: anytype) void { 180 | var data = hdr; 181 | switch (@typeInfo(@TypeOf(data))) { 182 | .Pointer => |ptr| { 183 | switch (@typeInfo(ptr.child)) { 184 | .Struct => |strct| { 185 | inline for (strct.fields) |field| { 186 | switch (@typeInfo(field.type)) { 187 | .Int => { 188 | @field(data.*, field.name) = std.mem.nativeToLittle( 189 | field.type, 190 | @field(data.*, field.name), 191 | ); 192 | }, 193 | else => {}, 194 | } 195 | } 196 | }, 197 | else => unreachable, 198 | } 199 | }, 200 | else => unreachable, 201 | } 202 | } 203 | 204 | fn writeHeader(header: Header, strm: anytype) !void { 205 | switch (header) { 206 | inline else => |hdr| { 207 | var hdr_mut = hdr; 208 | headerToLittle(&hdr_mut); 209 | try strm.writeAll(std.mem.asBytes(&hdr_mut)); 210 | } 211 | } 212 | } 213 | 214 | fn readHeader(idx: TagType, strm: anytype) !Header { 215 | const fields = @typeInfo(Header).Union.fields; 216 | const options = @typeInfo(@typeInfo(Header).Union.tag_type orelse unreachable).Enum.fields; 217 | 218 | inline for (fields) |f| { 219 | inline for (options) |o| { 220 | if (std.mem.eql(u8, o.name, f.name)) { 221 | if (o.value == idx) { 222 | var opt: f.type = undefined; 223 | _ = try strm.readAll(std.mem.asBytes(&opt)); 224 | return @unionInit(Header, f.name, opt); 225 | } 226 | } 227 | } 228 | } 229 | 230 | return .{ 231 | .Corrupt = .{ 232 | .tag = idx, 233 | }, 234 | }; 235 | } 236 | 237 | pub fn writeMessage(mes: Message, strm: anytype) !void { 238 | const idx = std.mem.nativeToLittle(TagType, @intFromEnum(mes.header)); 239 | try strm.writeAll(std.mem.asBytes(&idx)); 240 | try writeHeader(mes.header, strm); 241 | } 242 | 243 | pub fn readMessage(strm: anytype) !Message { 244 | const idx = try strm.readIntLittle(TagType); 245 | const header = try readHeader(idx, strm); 246 | return Message{ 247 | .header = header, 248 | }; 249 | } 250 | -------------------------------------------------------------------------------- /src/repl.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | const config_mod = @import("config.zig"); 4 | const client_mod = @import("client.zig"); 5 | const log = @import("log.zig"); 6 | 7 | const Config = config_mod.Config; 8 | const Client = client_mod.Client; 9 | const ServerError = client_mod.ServerError; 10 | const Entry = client_mod.Entry; 11 | 12 | const CliError = error{ 13 | NotEnoughArgs, 14 | UnknownCommand, 15 | }; 16 | 17 | const CommandCallback = *const fn (self: *Self) anyerror!void; 18 | 19 | const Command = struct { 20 | name: []const u8, 21 | description: []const u8, 22 | callback: CommandCallback, 23 | }; 24 | 25 | fn commandlessThan(ctx: void, lhs: Command, rhs: Command) bool { 26 | _ = ctx; 27 | return std.mem.lessThan(u8, lhs.name, rhs.name); 28 | } 29 | 30 | const Self = @This(); 31 | const CommandMap = std.StringArrayHashMap(Command); 32 | 33 | config: Config, 34 | client: Client(net.Stream), 35 | params: std.ArrayList([]const u8), 36 | is_exiting: bool, 37 | command_table: CommandMap, 38 | 39 | fn split(self: *Self, line: []const u8) !void { 40 | errdefer self.params.clearAndFree(); 41 | var iter = std.mem.splitAny(u8, line, " \t\n"); 42 | while (iter.next()) |word| { 43 | if (word.len > 0) { 44 | try self.params.append(word); 45 | } 46 | } 47 | } 48 | 49 | fn next(self: *Self) ![]const u8 { 50 | if (self.params.items.len == 0) 51 | return error.NotEnoughArgs; 52 | return self.params.orderedRemove(0); 53 | } 54 | 55 | fn makePrompt(self: *Self) ![]const u8 { 56 | const cwd = try self.client.getCwdAlloc(self.config.allocator); 57 | defer self.config.allocator.free(cwd); 58 | const prompt = try std.fmt.allocPrint(self.config.allocator, "{s}> ", .{cwd}); 59 | errdefer self.config.allocator.free(prompt); 60 | return prompt; 61 | } 62 | 63 | fn exec(self: *Self) !void { 64 | if (self.params.items.len == 0) 65 | return; 66 | const command = try self.next(); 67 | const entry = self.command_table.get(command) orelse return error.UnknownCommand; 68 | try entry.callback(self); 69 | } 70 | 71 | fn command_cd(self: *Self) !void { 72 | const path = try self.next(); 73 | try self.client.setCwd(path); 74 | } 75 | fn command_pwd(self: *Self) !void { 76 | const path = try self.client.getCwdAlloc(self.config.allocator); 77 | defer self.config.allocator.free(path); 78 | log.println(path); 79 | } 80 | fn command_quit(self: *Self) !void { 81 | self.is_exiting = true; 82 | } 83 | fn command_ping(self: *Self) !void { 84 | try self.client.ping(); 85 | log.println("the server is up"); 86 | } 87 | fn command_cat(self: *Self) !void { 88 | _ = try self.client.getFile(try self.next(), std.io.getStdOut().writer()); 89 | } 90 | fn command_get(self: *Self) !void { 91 | const remote_path = try self.next(); 92 | const local_path = try self.next(); 93 | const local_file = std.fs.cwd().createFile(local_path, .{}) catch { 94 | log.errPrintFmt("failed to open local file: {s}\n", .{local_path}); 95 | return; 96 | }; 97 | defer local_file.close(); 98 | _ = try self.client.getFile(remote_path, local_file.writer()); 99 | } 100 | fn command_stat(self: *Self) !void { 101 | const remote_path = try self.next(); 102 | const stt = try self.client.stat(remote_path); 103 | switch (stt) { 104 | .File => |hdr| { 105 | log.printFmt("type: file\nsize: {d}\n", .{hdr.size}); 106 | }, 107 | .Dir => { 108 | log.printFmt("type: directory\n", .{}); 109 | }, 110 | } 111 | } 112 | fn command_put(self: *Self) !void { 113 | const remote_path = try self.next(); 114 | const local_path = try self.next(); 115 | const local_file = std.fs.cwd().openFile(local_path, .{}) catch { 116 | log.errPrintFmt("failed to open local file: {s}\n", .{local_path}); 117 | return; 118 | }; 119 | const local_file_stat = try local_file.stat(); 120 | defer local_file.close(); 121 | _ = try self.client.putFile(remote_path, local_file.reader(), local_file_stat.size); 122 | } 123 | fn command_delete(self: *Self) !void { 124 | const remote_path = try self.next(); 125 | try self.client.deleteFile(remote_path); 126 | } 127 | fn command_help(self: *Self) !void { 128 | var iter = self.command_table.iterator(); 129 | log.println(""); 130 | while (iter.next()) |entry| { 131 | log.printFmt("{s}\t\t\t{s}\n", .{ 132 | entry.key_ptr.*, 133 | entry.value_ptr.*.description, 134 | }); 135 | } 136 | log.println(""); 137 | } 138 | fn command_ls(self: *Self) !void { 139 | try self.client.getEntries(self.next() catch "."); 140 | var buf = [_]u8{0} ** 256; 141 | var entry = Entry{ 142 | .name_buffer = buf[0..], 143 | .name = undefined, 144 | .is_dir = undefined, 145 | }; 146 | while (try self.client.readEntry(&entry)) { 147 | log.println(entry.name); 148 | } 149 | } 150 | 151 | fn runloop(self: *Self) !void { 152 | // print prompt 153 | const prompt = try self.makePrompt(); 154 | defer self.config.allocator.free(prompt); 155 | log.print(prompt); 156 | // read line 157 | var line_buffer = [_]u8{0} ** 0x1000; 158 | const len = try std.io.getStdIn().read(line_buffer[0..]); 159 | // exit on ^D 160 | if (len == 0) { 161 | self.is_exiting = true; 162 | return; 163 | } 164 | // execute 165 | try self.split(line_buffer[0..len]); 166 | defer self.params.clearAndFree(); 167 | try self.exec(); 168 | } 169 | 170 | fn connect(self: *Self, config: Config) !void { 171 | const stream = try net.tcpConnectToHost(config.allocator, config.address, config.port); 172 | self.client = Client(net.Stream).init(); 173 | try self.client.connect(stream); 174 | } 175 | 176 | pub fn mainloop(self: *Self, config: Config) !void { 177 | try self.installCommands(); 178 | try self.connect(config); 179 | while (!self.is_exiting) { 180 | self.runloop() catch |err| { 181 | const error_text = switch (err) { 182 | error.UnknownCommand, 183 | error.NotEnoughArgs, 184 | error.NonExisting, 185 | error.IsNotFile, 186 | error.IsNotDir, 187 | error.AccessDenied, 188 | error.CantOpen, 189 | error.InvalidFileName, 190 | => @errorName(err), 191 | error.EndOfStream, 192 | error.connectionResetByPeer, 193 | => { 194 | while (true) { 195 | self.connect(config) catch { 196 | log.println("trying to reconnect..."); 197 | std.time.sleep(3 * 1000 * 1000 * 1000); 198 | continue; 199 | }; 200 | break; 201 | } 202 | continue; 203 | }, 204 | else => return err, 205 | }; 206 | log.printFmt("Error: {s}\n", .{error_text}); 207 | continue; 208 | }; 209 | } 210 | try self.client.close(); 211 | } 212 | 213 | var commands_list = [_]Command{ 214 | .{ 215 | .name = "put", 216 | .description = "put | upload file to server", 217 | .callback = command_put, 218 | }, 219 | .{ 220 | .name = "get", 221 | .description = "get | download file from server", 222 | .callback = command_get, 223 | }, 224 | .{ 225 | .name = "ls", 226 | .description = "ls [dir] | shows entries in CWD or dir", 227 | .callback = command_ls, 228 | }, 229 | .{ 230 | .name = "quit", 231 | .description = "close connection", 232 | .callback = command_quit, 233 | }, 234 | .{ 235 | .name = "ping", 236 | .description = "check whether server is up or not", 237 | .callback = command_ping, 238 | }, 239 | .{ 240 | .name = "cat", 241 | .description = "cat | print file content to terminal", 242 | .callback = command_cat, 243 | }, 244 | .{ 245 | .name = "pwd", 246 | .description = "show CWD", 247 | .callback = command_pwd, 248 | }, 249 | .{ 250 | .name = "cd", 251 | .description = "cd | change CWD to dir", 252 | .callback = command_cd, 253 | }, 254 | .{ 255 | .name = "delete", 256 | .description = "delete | delete file", 257 | .callback = command_delete, 258 | }, 259 | .{ 260 | .name = "stat", 261 | .description = "stat | get stat of inode", 262 | .callback = command_stat, 263 | }, 264 | .{ 265 | .name = "help", 266 | .description = "print this help", 267 | .callback = command_help, 268 | }, 269 | }; 270 | 271 | fn installCommands(self: *Self) !void { 272 | std.mem.sort( 273 | Command, 274 | commands_list[0..], 275 | {}, 276 | commandlessThan, 277 | ); 278 | for (commands_list) |cmd| { 279 | try self.command_table.put(cmd.name, cmd); 280 | } 281 | } 282 | 283 | pub fn init(config: Config) Self { 284 | return .{ 285 | .config = config, 286 | .is_exiting = false, 287 | .client = undefined, 288 | .params = std.ArrayList([]const u8).init(config.allocator), 289 | .command_table = CommandMap.init(config.allocator), 290 | }; 291 | } 292 | -------------------------------------------------------------------------------- /src/client.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const protocol = @import("protocol.zig"); 3 | const net = std.net; 4 | const Header = protocol.Header; 5 | pub const ServerError = protocol.ServerError; 6 | 7 | pub const Error = error{ 8 | Protocol, 9 | AlreadyConnected, 10 | NotConnected, 11 | ReadingEntry, 12 | NotReadingEntry, 13 | } || ServerError; 14 | 15 | pub const Entry = struct { 16 | name_buffer: []u8, 17 | name: []u8, 18 | is_dir: bool, 19 | }; 20 | 21 | pub const Stat = union(enum) { 22 | File: struct { size: u64 }, 23 | Dir: struct {}, 24 | }; 25 | 26 | pub fn Client(comptime T: type) type { 27 | return struct { 28 | const Self = @This(); 29 | const DATA_BUFFER_SIZE = 0x1000; 30 | 31 | is_reading_entries: bool, 32 | stream: ?T, 33 | server_error_arg1: u32, 34 | server_error_arg2: u32, 35 | server_info: ?protocol.InfoHeader, 36 | 37 | fn send(self: *Self, header: Header) !void { 38 | try protocol.writeMessage(.{ 39 | .header = header, 40 | }, self.stream.?.writer()); 41 | } 42 | fn recv(self: *Self) !Header { 43 | const mes = try protocol.readMessage(self.stream.?.reader()); 44 | try self.check(mes.header); 45 | return mes.header; 46 | } 47 | fn recvOk(self: *Self) !void { 48 | switch (try self.recv()) { 49 | .Ok => {}, 50 | else => return error.Protocol, 51 | } 52 | } 53 | fn check(self: *Self, header: Header) !void { 54 | switch (header) { 55 | .Error => |hdr| { 56 | self.server_error_arg1 = hdr.arg1; 57 | self.server_error_arg2 = hdr.arg2; 58 | const err = protocol.decodeServerError(hdr.code); 59 | switch (err) { 60 | error.Unrecognized => return error.Protocol, 61 | else => return err, 62 | } 63 | }, 64 | .Corrupt => return error.Protocol, 65 | else => {}, 66 | } 67 | } 68 | fn checkConnected(self: *const Self) !void { 69 | if (self.stream == null) { 70 | return error.NotConnected; 71 | } 72 | if (self.is_reading_entries) { 73 | return error.ReadingEntry; 74 | } 75 | } 76 | fn checkReadingEntry(self: *const Self) !void { 77 | if (!self.is_reading_entries) { 78 | return error.NotReadingEntry; 79 | } 80 | } 81 | fn readPayload(self: *Self, buffer: ?[]u8, length: usize) !usize { 82 | if (buffer) |buf| { 83 | if (length > buf.len) { 84 | return self.stream.?.reader().readAll(buf); 85 | } else { 86 | return self.stream.?.reader().readAll(buf[0..length]); 87 | } 88 | } else { 89 | try self.stream.?.reader().skipBytes(length, .{}); 90 | return 0; 91 | } 92 | } 93 | fn writeBuffer(self: *Self, buffer: []const u8) !void { 94 | return self.stream.?.writer().writeAll(buffer); 95 | } 96 | 97 | pub fn init() Self { 98 | return .{ 99 | .is_reading_entries = false, 100 | .stream = null, 101 | .server_error_arg1 = 0, 102 | .server_error_arg2 = 0, 103 | .server_info = null, 104 | }; 105 | } 106 | pub fn connect(self: *Self, stream: T) !void { 107 | if (self.stream == null) { 108 | self.stream = stream; 109 | } else { 110 | return error.AlreadyConnected; 111 | } 112 | } 113 | pub fn ping(self: *Self) !void { 114 | try self.send(.{ 115 | .Ping = .{}, 116 | }); 117 | switch (try self.recv()) { 118 | .PingReply => {}, 119 | else => {}, 120 | } 121 | } 122 | pub fn info(self: *Self) !protocol.InfoHeader { 123 | if (self.server_info) |inf| { 124 | return inf; 125 | } 126 | try self.send( 127 | .{ 128 | .GetInfo = .{}, 129 | }, 130 | ); 131 | self.server_info = switch (try self.recv()) { 132 | .Info => |hdr| hdr, 133 | else => return error.Protocol, 134 | }; 135 | return self.info(); 136 | } 137 | pub fn setCwd(self: *Self, path: []const u8) !void { 138 | try self.checkConnected(); 139 | try self.send( 140 | .{ 141 | .Cd = .{ 142 | .length = @intCast(path.len), 143 | }, 144 | }, 145 | ); 146 | _ = try self.writeBuffer(path); 147 | const resp = try self.recv(); 148 | switch (resp) { 149 | .Ok => {}, 150 | else => return error.Protocol, 151 | } 152 | } 153 | pub fn getCwd(self: *Self, buffer: []u8) !void { 154 | try self.checkConnected(); 155 | try self.send( 156 | .{ 157 | .Pwd = .{}, 158 | }, 159 | ); 160 | const resp = try self.recv(); 161 | switch (resp) { 162 | .Path => |hdr| { 163 | _ = try self.readPayload(buffer, hdr.length); 164 | }, 165 | else => return error.Protocol, 166 | } 167 | } 168 | pub fn getCwdAlloc(self: *Self, allocator: std.mem.Allocator) ![]u8 { 169 | try self.checkConnected(); 170 | try self.send( 171 | .{ 172 | .Pwd = .{}, 173 | }, 174 | ); 175 | const resp = try self.recv(); 176 | switch (resp) { 177 | .Path => |hdr| { 178 | var buffer = try allocator.alloc(u8, hdr.length); 179 | _ = try self.readPayload(buffer, hdr.length); 180 | return buffer; 181 | }, 182 | else => return error.Protocol, 183 | } 184 | } 185 | pub fn getFile(self: *Self, path: []const u8, writer: anytype) !usize { 186 | try self.checkConnected(); 187 | try self.send( 188 | .{ 189 | .Read = .{ 190 | .length = @intCast(path.len), 191 | }, 192 | }, 193 | ); 194 | _ = try self.writeBuffer(path); 195 | const resp = try self.recv(); 196 | switch (resp) { 197 | .File => |hdr| { 198 | var buf = [_]u8{0} ** DATA_BUFFER_SIZE; 199 | var rem = hdr.size; 200 | while (rem > 0) { 201 | const expected = @min(rem, buf.len); 202 | const count = try self.readPayload(buf[0..expected], expected); 203 | _ = try writer.writeAll(buf[0..count]); 204 | if (count != expected) { 205 | return error.Protocol; 206 | } 207 | rem -= expected; 208 | } 209 | return hdr.size; 210 | }, 211 | else => return error.Protocol, 212 | } 213 | } 214 | pub fn stat(self: *Self, path: []const u8) !Stat { 215 | try self.checkConnected(); 216 | try self.send( 217 | .{ 218 | .GetStat = .{ 219 | .length = @intCast(path.len), 220 | }, 221 | }, 222 | ); 223 | _ = try self.writeBuffer(path); 224 | const resp = try self.recv(); 225 | return switch (resp) { 226 | .Stat => |hdr| switch (hdr.ty) { 227 | .File => Stat{ .File = .{ .size = hdr.size } }, 228 | .Dir => Stat{ .Dir = .{} }, 229 | }, 230 | else => return error.Protocol, 231 | }; 232 | } 233 | pub fn putFile(self: *Self, path: []const u8, reader: anytype, size: u64) !void { 234 | try self.checkConnected(); 235 | try self.send( 236 | .{ 237 | .Write = .{ 238 | .length = @intCast(path.len), 239 | }, 240 | }, 241 | ); 242 | _ = try self.writeBuffer(path); 243 | try self.recvOk(); 244 | try self.send(.{ .File = .{ .size = size } }); 245 | var buf = [_]u8{0} ** DATA_BUFFER_SIZE; 246 | var rem = size; 247 | while (rem > 0) { 248 | const expected = @min(rem, buf.len); 249 | const count = try reader.readAll(buf[0..expected]); 250 | try self.writeBuffer(buf[0..count]); 251 | rem -= expected; 252 | } 253 | try self.recvOk(); 254 | } 255 | pub fn deleteFile(self: *Self, path: []const u8) !void { 256 | try self.checkConnected(); 257 | try self.send( 258 | .{ 259 | .Delete = .{ 260 | .length = @intCast(path.len), 261 | }, 262 | }, 263 | ); 264 | _ = try self.writeBuffer(path); 265 | try self.recvOk(); 266 | } 267 | pub fn getEntriesAlloc(self: *Self, path: []const u8, allocator: std.mem.Allocator) !std.ArrayList(Entry) { 268 | try self.checkConnected(); 269 | try self.send(.{ 270 | .List = .{ 271 | .length = @intCast(path.len), 272 | }, 273 | }); 274 | _ = try self.writeBuffer(path); 275 | switch (try self.recv()) { 276 | .Ok => {}, 277 | else => return error.Protocol, 278 | } 279 | var list = std.ArrayList(Entry).init(allocator); 280 | errdefer { 281 | for (list.items) |ele| { 282 | allocator.free(ele.name); 283 | } 284 | list.deinit(); 285 | } 286 | while (true) { 287 | switch (try self.recv()) { 288 | .Entry => |hdr| { 289 | var name = try allocator.alloc(u8, hdr.length); 290 | _ = try self.readPayload(name, hdr.length); 291 | try list.append(.{ 292 | .name_buffer = name, 293 | .name = name, 294 | .is_dir = hdr.is_dir, 295 | }); 296 | }, 297 | .End => break, 298 | else => return error.Protocol, 299 | } 300 | } 301 | return list; 302 | } 303 | pub fn getEntries(self: *Self, path: []const u8) !void { 304 | try self.checkConnected(); 305 | try self.send(.{ 306 | .List = .{ 307 | .length = @intCast(path.len), 308 | }, 309 | }); 310 | _ = try self.writeBuffer(path); 311 | switch (try self.recv()) { 312 | .Ok => {}, 313 | else => return error.Protocol, 314 | } 315 | self.is_reading_entries = true; 316 | } 317 | pub fn readEntry(self: *Self, entry: ?*Entry) !bool { 318 | try self.checkReadingEntry(); 319 | switch (try self.recv()) { 320 | .Entry => |hdr| { 321 | if (entry) |ent| { 322 | ent.is_dir = hdr.is_dir; 323 | const len = try self.readPayload(ent.name_buffer, hdr.length); 324 | ent.name = ent.name_buffer[0..len]; 325 | } else { 326 | _ = try self.readPayload(null, hdr.length); 327 | } 328 | }, 329 | .End => { 330 | self.is_reading_entries = false; 331 | }, 332 | else => return error.Protocol, 333 | } 334 | return self.is_reading_entries; 335 | } 336 | pub fn abortReadingEntry(self: *Self) !void { 337 | try self.checkReadingEntry(); 338 | while (self.is_reading_entries) 339 | _ = try self.readEntry(null); 340 | } 341 | pub fn close(self: *Self) !void { 342 | if (self.is_reading_entries) 343 | try self.abortReadingEntry(); 344 | try self.checkConnected(); 345 | try self.send(.{ 346 | .Quit = .{}, 347 | }); 348 | switch (try self.recv()) { 349 | .QuitReply => {}, 350 | else => return error.Protocol, 351 | } 352 | self.stream = null; 353 | } 354 | }; 355 | } 356 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | const config_mod = @import("config.zig"); 4 | const protocol = @import("protocol.zig"); 5 | const log = @import("log.zig"); 6 | const client_mod = @import("client.zig"); 7 | const server_mod = @import("server.zig"); 8 | 9 | const Header = protocol.Header; 10 | const Config = config_mod.Config; 11 | const Message = protocol.Message; 12 | const ServerError = protocol.ServerError; 13 | const CdHeader = protocol.CdHeader; 14 | const ListHeader = protocol.ListHeader; 15 | const ReadHeader = protocol.ReadHeader; 16 | const Client = client_mod.Client; 17 | const Server = server_mod.Server; 18 | const ServerHandler = server_mod.ServerHandler; 19 | 20 | // The tests for both the server and the client. 21 | 22 | /// A naive implementation of a single-producer single-consumer inter-thread communication. 23 | /// This is just for the purpose of testing and thus no real performance is required. 24 | const Channel = struct { 25 | const Self = @This(); 26 | const DEFAULT_SIZE = 0x1000; 27 | 28 | /// notifies the reader for incoming bytes 29 | read_sem: std.Thread.Semaphore, 30 | /// notifies the writer for free-space 31 | write_sem: std.Thread.Semaphore, 32 | /// control access to underlying ring buffer 33 | mtx: std.Thread.Mutex, 34 | buffer: std.RingBuffer, 35 | 36 | pub fn init(allocator: std.mem.Allocator) !Self { 37 | return .{ 38 | .mtx = std.Thread.Mutex{}, 39 | .read_sem = std.Thread.Semaphore{}, 40 | .write_sem = std.Thread.Semaphore{ 41 | .permits = DEFAULT_SIZE, 42 | }, 43 | .buffer = try std.RingBuffer.init(allocator, DEFAULT_SIZE), 44 | }; 45 | } 46 | pub fn deinit(self: *Self, allocator: std.mem.Allocator) void { 47 | self.buffer.deinit(allocator); 48 | } 49 | pub fn readByte(self: *Self) u8 { 50 | self.read_sem.wait(); 51 | self.mtx.lock(); 52 | const byte = self.buffer.read().?; 53 | self.mtx.unlock(); 54 | self.write_sem.post(); 55 | return byte; 56 | } 57 | pub fn writeByte(self: *Self, byte: u8) void { 58 | self.write_sem.wait(); 59 | self.mtx.lock(); 60 | self.buffer.write(byte) catch unreachable; 61 | self.mtx.unlock(); 62 | self.read_sem.post(); 63 | } 64 | }; 65 | 66 | /// holds references to two channels. 67 | /// Uses one for sending & the other for recieving data. 68 | const ChannelStream = struct { 69 | const Self = @This(); 70 | const Reader = std.io.Reader(*Self, std.os.ReadError, read); 71 | const Writer = std.io.Writer(*Self, std.os.WriteError, write); 72 | 73 | sender: *Channel, 74 | reciever: *Channel, 75 | 76 | pub fn read(self: *Self, buf: []u8) std.os.ReadError!usize { 77 | for (buf) |*byte| { 78 | byte.* = self.reciever.readByte(); 79 | } 80 | return buf.len; 81 | } 82 | pub fn write(self: *Self, buf: []const u8) std.os.WriteError!usize { 83 | for (buf) |byte| { 84 | self.sender.writeByte(byte); 85 | } 86 | return buf.len; 87 | } 88 | pub fn reader(self: *Self) Reader { 89 | return .{ .context = self }; 90 | } 91 | pub fn writer(self: *Self) Writer { 92 | return .{ .context = self }; 93 | } 94 | }; 95 | 96 | /// Upon initialization, creates a thread which runs a single ServerHandler. 97 | /// The server and client communicate over channels. 98 | /// Call deinit to `join` the thread. 99 | /// 100 | /// Why test like this? 101 | /// 102 | /// This allows the tests to be fully independent. Running a TCP server for 103 | /// every test makes it impossible to parallelize tests, as multiple servers 104 | /// can not listen on a single port number. 105 | /// 106 | /// Running a server instance for all the tests is also not practical; as failure 107 | /// in one test might corrupt the server's global state and cause failure in 108 | /// other tests. 109 | const ServerTester = struct { 110 | thread: std.Thread, 111 | // server to client 112 | inner_cts: *Channel, 113 | // client to server 114 | inner_stc: *Channel, 115 | stream: ChannelStream, 116 | allocator: std.mem.Allocator, 117 | 118 | const Self = @This(); 119 | 120 | /// this runs in another thread 121 | fn runServer(config: Config, channel: ChannelStream) !void { 122 | var channel_mut = channel; 123 | // create a handler instance that operates on a channel 124 | var handler = ServerHandler(ChannelStream).init(&config, &channel_mut); 125 | try handler.handleClient(); 126 | } 127 | 128 | fn init(config: Config) !Self { 129 | const stc = try config.allocator.create(Channel); 130 | const cts = try config.allocator.create(Channel); 131 | stc.* = try Channel.init(config.allocator); 132 | cts.* = try Channel.init(config.allocator); 133 | const server = ChannelStream{ 134 | .sender = stc, 135 | .reciever = cts, 136 | }; 137 | const client = ChannelStream{ 138 | .sender = cts, 139 | .reciever = stc, 140 | }; 141 | const thread = try std.Thread.spawn(.{}, runServer, .{ config, server }); 142 | return .{ 143 | .thread = thread, 144 | .inner_stc = stc, 145 | .inner_cts = cts, 146 | .stream = client, 147 | .allocator = config.allocator, 148 | }; 149 | } 150 | fn deinit(self: *Self) void { 151 | self.inner_cts.deinit(self.allocator); 152 | self.inner_stc.deinit(self.allocator); 153 | self.allocator.destroy(self.inner_cts); 154 | self.allocator.destroy(self.inner_stc); 155 | } 156 | fn recv(self: *Self) !Message { 157 | return try protocol.readMessage(self.stream.reader()); 158 | } 159 | fn send(self: *Self, mes: Message) !void { 160 | try protocol.writeMessage(mes, self.stream.writer()); 161 | } 162 | }; 163 | 164 | const testdir = "testdir"; 165 | 166 | fn makeTestDir() ![]u8 { 167 | const uuid = std.crypto.random.int(u128); 168 | const path = try std.fmt.allocPrint( 169 | std.testing.allocator, 170 | "{s}/{d}", 171 | .{ testdir, uuid }, 172 | ); 173 | try std.fs.cwd().makePath(path); 174 | return path; 175 | } 176 | 177 | fn makeTestFile(dir_path: []const u8, file_path: []const u8, content: []const u8) !void { 178 | const dir = try std.fs.cwd().openDir(dir_path, .{}); 179 | const file = try dir.createFile(file_path, .{}); 180 | _ = try file.write(content); 181 | } 182 | 183 | fn removeTestDir(name: []const u8) void { 184 | std.fs.cwd().deleteTree(name) catch {}; 185 | std.testing.allocator.free(name); 186 | } 187 | 188 | fn expectCwd(client: *Client(ChannelStream), exp: []const u8) !void { 189 | const pwd = try client.getCwdAlloc(std.testing.allocator); 190 | defer std.testing.allocator.free(pwd); 191 | try std.testing.expectEqualSlices(u8, exp, pwd); 192 | } 193 | fn expectFile(path: []const u8, data: []const u8) !void { 194 | try std.testing.expect(data.len < 256); 195 | var buf = [_]u8{0} ** 256; 196 | const file = try std.fs.cwd().openFile(path, .{}); 197 | defer file.close(); 198 | const count = try file.readAll(buf[0..data.len]); 199 | try std.testing.expectEqualSlices(u8, data, buf[0..count]); 200 | } 201 | 202 | // this simple test shows how to test the server & client 203 | test "ping" { 204 | // run the server in a new thread 205 | var server = try ServerTester.init(Config.init(std.testing.allocator)); 206 | // join the thread 207 | defer server.deinit(); 208 | 209 | // create the client instance that operates on a channel 210 | var client = Client(ChannelStream).init(); 211 | try client.connect(server.stream); 212 | 213 | // use client as usual 214 | try client.ping(); 215 | 216 | // send the 217 | try client.close(); 218 | } 219 | 220 | test "corrupt tag" { 221 | var server = try ServerTester.init(Config.init(std.testing.allocator)); 222 | defer server.deinit(); 223 | const corrupt_tag = std.mem.nativeToLittle(u16, 0xeeee); 224 | _ = try server.stream.write(std.mem.asBytes(&corrupt_tag)); 225 | const resp = try server.recv(); 226 | try std.testing.expectEqual(Message{ 227 | .header = .{ 228 | .Error = .{ 229 | .code = protocol.encodeServerError(ServerError.CorruptMessageTag), 230 | .arg1 = corrupt_tag, 231 | .arg2 = 0, 232 | }, 233 | }, 234 | }, resp); 235 | } 236 | 237 | test "invalid commands" { 238 | var server = try ServerTester.init(Config.init(std.testing.allocator)); 239 | defer server.deinit(); 240 | const mes = Message{ 241 | .header = .{ 242 | .Ok = .{}, 243 | }, 244 | }; 245 | try server.send(mes); 246 | const resp = try server.recv(); 247 | try std.testing.expectEqual(Message{ 248 | .header = .{ 249 | .Error = .{ 250 | .code = protocol.encodeServerError(ServerError.UnexpectedMessage), 251 | .arg1 = @intFromEnum(mes.header), 252 | .arg2 = 0, 253 | }, 254 | }, 255 | }, resp); 256 | } 257 | 258 | test "info" { 259 | var server = try ServerTester.init(Config.init(std.testing.allocator)); 260 | defer server.deinit(); 261 | 262 | var client = Client(ChannelStream).init(); 263 | try client.connect(server.stream); 264 | 265 | const info = try client.info(); 266 | try std.testing.expectEqual(protocol.InfoHeader{ 267 | .max_name = std.fs.MAX_NAME_BYTES, 268 | .max_path = std.fs.MAX_PATH_BYTES, 269 | }, info); 270 | 271 | try client.close(); 272 | } 273 | 274 | test "working directory" { 275 | const allocator = std.testing.allocator; 276 | const temp_dir = try makeTestDir(); 277 | defer removeTestDir(temp_dir); 278 | var server = try ServerTester.init(Config.init(allocator)); 279 | defer server.deinit(); 280 | 281 | var client = Client(ChannelStream).init(); 282 | try client.connect(server.stream); 283 | 284 | try client.setCwd(temp_dir); 285 | const exp_pwd = try std.fmt.allocPrint(std.testing.allocator, "/{s}", .{temp_dir}); 286 | defer std.testing.allocator.free(exp_pwd); 287 | try expectCwd(&client, exp_pwd); 288 | try client.setCwd("../../.."); 289 | try expectCwd(&client, "/"); 290 | client.setCwd(testdir ++ "/non-existing") catch |err| { 291 | switch (err) { 292 | error.NonExisting => {}, 293 | else => unreachable, 294 | } 295 | }; 296 | try expectCwd(&client, "/"); 297 | try client.setCwd(temp_dir); 298 | try client.setCwd("/"); 299 | try expectCwd(&client, "/"); 300 | 301 | try client.close(); 302 | } 303 | 304 | test "read file" { 305 | const allocator = std.testing.allocator; 306 | const temp_dir = try makeTestDir(); 307 | defer removeTestDir(temp_dir); 308 | const file_name = "test-file"; 309 | const file_content: []const u8 = "some data"; 310 | const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ temp_dir, file_name }); 311 | defer allocator.free(file_path); 312 | try makeTestFile(temp_dir, file_name, file_content); 313 | 314 | var server = try ServerTester.init(Config.init(allocator)); 315 | defer server.deinit(); 316 | var buf = [_]u8{0} ** 64; 317 | var buf_stream = std.io.fixedBufferStream(buf[0..]); 318 | 319 | var client = Client(ChannelStream).init(); 320 | try client.connect(server.stream); 321 | try std.testing.expectError( 322 | error.IsNotFile, 323 | client.getFile( 324 | temp_dir, 325 | buf_stream.writer(), 326 | ), 327 | ); 328 | const size = try client.getFile( 329 | file_path, 330 | buf_stream.writer(), 331 | ); 332 | try client.close(); 333 | try std.testing.expectEqualSlices(u8, "some data", buf[0..size]); 334 | } 335 | 336 | test "stat file" { 337 | const allocator = std.testing.allocator; 338 | const temp_dir = try makeTestDir(); 339 | defer removeTestDir(temp_dir); 340 | const file_name = "test-file"; 341 | const file_content: []const u8 = "some data"; 342 | const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ temp_dir, file_name }); 343 | defer allocator.free(file_path); 344 | try makeTestFile(temp_dir, file_name, file_content); 345 | 346 | var server = try ServerTester.init(Config.init(allocator)); 347 | defer server.deinit(); 348 | 349 | var client = Client(ChannelStream).init(); 350 | try client.connect(server.stream); 351 | try std.testing.expectError( 352 | error.NonExisting, 353 | client.stat("213724#@#%&"), 354 | ); 355 | switch (try client.stat(file_path)) { 356 | .File => |hdr| try std.testing.expectEqual( 357 | file_content.len, 358 | hdr.size, 359 | ), 360 | else => unreachable, 361 | } 362 | switch (try client.stat(temp_dir)) { 363 | .Dir => {}, 364 | else => unreachable, 365 | } 366 | try client.close(); 367 | } 368 | 369 | test "write file" { 370 | const allocator = std.testing.allocator; 371 | const temp_dir = try makeTestDir(); 372 | defer removeTestDir(temp_dir); 373 | const file_name = "test-file"; 374 | const file_content: []const u8 = "some data"; 375 | const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ temp_dir, file_name }); 376 | defer allocator.free(file_path); 377 | 378 | var server = try ServerTester.init(Config.init(allocator)); 379 | defer server.deinit(); 380 | var buf_stream = std.io.fixedBufferStream(file_content[0..]); 381 | 382 | var client = Client(ChannelStream).init(); 383 | try client.connect(server.stream); 384 | try client.putFile( 385 | file_path, 386 | buf_stream.reader(), 387 | file_content.len, 388 | ); 389 | try client.close(); 390 | try expectFile(file_path, file_content); 391 | } 392 | 393 | test "delete file" { 394 | const allocator = std.testing.allocator; 395 | const temp_dir = try makeTestDir(); 396 | defer removeTestDir(temp_dir); 397 | const file_name = "test-file"; 398 | const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ temp_dir, file_name }); 399 | defer allocator.free(file_path); 400 | try makeTestFile(temp_dir, file_name, ""); 401 | 402 | var server = try ServerTester.init(Config.init(allocator)); 403 | defer server.deinit(); 404 | 405 | var client = Client(ChannelStream).init(); 406 | try client.connect(server.stream); 407 | try client.deleteFile(file_path); 408 | try client.close(); 409 | expectFile(file_path, "") catch |err| { 410 | switch (err) { 411 | error.FileNotFound => {}, 412 | else => unreachable, 413 | } 414 | }; 415 | } 416 | 417 | test "list of files" { 418 | const allocator = std.testing.allocator; 419 | const temp_dir = try makeTestDir(); 420 | defer removeTestDir(temp_dir); 421 | var files = [_]struct { 422 | path: []const u8, 423 | recieved: bool, 424 | }{ 425 | .{ .path = "file1", .recieved = false }, 426 | .{ .path = "file2", .recieved = false }, 427 | .{ .path = "file3", .recieved = false }, 428 | }; 429 | for (files) |file| { 430 | try makeTestFile(temp_dir, file.path, ""); 431 | } 432 | var server = try ServerTester.init(Config.init(allocator)); 433 | defer server.deinit(); 434 | 435 | var client = Client(ChannelStream).init(); 436 | try client.connect(server.stream); 437 | var entries = try client.getEntriesAlloc(temp_dir, std.testing.allocator); 438 | defer { 439 | for (entries.items) |entry| { 440 | std.testing.allocator.free(entry.name); 441 | } 442 | entries.deinit(); 443 | } 444 | 445 | for (entries.items) |entry| { 446 | var found = false; 447 | for (&files) |*file| { 448 | if (std.mem.eql(u8, file.path, entry.name)) { 449 | if (file.recieved) 450 | unreachable; 451 | file.recieved = true; 452 | found = true; 453 | break; 454 | } 455 | } 456 | if (!found) 457 | unreachable; 458 | } 459 | for (files) |file| { 460 | if (!file.recieved) { 461 | unreachable; 462 | } 463 | } 464 | try client.close(); 465 | } 466 | -------------------------------------------------------------------------------- /src/server.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const net = std.net; 3 | const config_mod = @import("config.zig"); 4 | const protocol = @import("protocol.zig"); 5 | const log = @import("log.zig"); 6 | 7 | const Header = protocol.Header; 8 | const Config = config_mod.Config; 9 | const Message = protocol.Message; 10 | const ServerError = protocol.ServerError; 11 | const CdHeader = protocol.CdHeader; 12 | const ListHeader = protocol.ListHeader; 13 | const ReadHeader = protocol.ReadHeader; 14 | const WriteHeader = protocol.WriteHeader; 15 | const DeleteHeader = protocol.DeleteHeader; 16 | const PathHeader = protocol.PathHeader; 17 | const NodeType = protocol.NodeType; 18 | 19 | /// Created per client. 20 | /// File/stream agnostic. 21 | /// Works on anything that implements reader() & writer(). 22 | pub fn ServerHandler(comptime T: type) type { 23 | return struct { 24 | const MAX_NAME = std.fs.MAX_NAME_BYTES; 25 | const MAX_PATH = std.fs.MAX_PATH_BYTES; 26 | const READ_BUFFER_SIZE = 0x1000; 27 | const Self = @This(); 28 | 29 | isExiting: bool, 30 | config: *const Config, 31 | cwd: std.fs.Dir, 32 | cwd_original: std.fs.Dir, 33 | stream: *T, 34 | depth: usize, 35 | error_arg1: u32, 36 | error_arg2: u32, 37 | 38 | pub fn init(config: *const Config, stream: *T) Self { 39 | const cwd = std.fs.cwd(); 40 | return .{ 41 | .isExiting = false, 42 | .config = config, 43 | .cwd_original = cwd, 44 | .cwd = copyDir(cwd), 45 | .stream = stream, 46 | .depth = 0, 47 | .error_arg1 = 0, 48 | .error_arg2 = 0, 49 | }; 50 | } 51 | 52 | fn deinit(self: *Self) void { 53 | self.cwd.close(); 54 | } 55 | 56 | fn sendError(self: *Self, err: ServerError) !void { 57 | try self.send( 58 | .{ 59 | .Error = .{ 60 | .code = protocol.encodeServerError(err), 61 | .arg1 = self.error_arg1, 62 | .arg2 = self.error_arg2, 63 | }, 64 | }, 65 | ); 66 | } 67 | 68 | fn send(self: *Self, header: Header) !void { 69 | try protocol.writeMessage(.{ 70 | .header = header, 71 | }, self.stream.writer()); 72 | } 73 | 74 | fn recv(self: *Self) !Message { 75 | return protocol.readMessage(self.stream.reader()); 76 | } 77 | 78 | /// This should ALWAYS be called instead of `std.fs.cwd().openDir()`. The 79 | /// server must act like a `chroot`ed process, and MUST NOT let the client 80 | /// peek into directories which are not in working tree of Server process. 81 | fn openDir(self: *Self, path: []const u8, dir_depth: ?*usize) !std.fs.Dir { 82 | var iter = try std.fs.path.componentIterator(path); 83 | var depth = self.depth; 84 | var dir = blk: { 85 | if (std.fs.path.isAbsolute(path)) { 86 | depth = 0; 87 | break :blk copyDir(self.cwd_original); 88 | } else { 89 | break :blk copyDir(self.cwd); 90 | } 91 | }; 92 | errdefer dir.close(); 93 | while (iter.next()) |seg| { 94 | if (seg.name.len == 0) 95 | continue; 96 | const is_dotdot = std.mem.eql(u8, seg.name, ".."); 97 | if (is_dotdot) { 98 | if (depth > 0) { 99 | depth -= 1; 100 | } else { 101 | continue; 102 | } 103 | } else { 104 | depth += 1; 105 | } 106 | 107 | const new_dir = dir.openDir(seg.name, .{ 108 | .no_follow = true, 109 | }) catch |err| { 110 | return switch (err) { 111 | error.FileNotFound => ServerError.NonExisting, 112 | error.NotDir => ServerError.IsNotDir, 113 | error.AccessDenied => ServerError.AccessDenied, 114 | else => ServerError.CantOpen, 115 | }; 116 | }; 117 | dir = new_dir; 118 | } 119 | 120 | if (dir_depth) |dd| { 121 | dd.* = depth; 122 | } 123 | return dir; 124 | } 125 | fn readPath(self: *Self, length: u16, buffer: []u8) ![]u8 { 126 | if (length > MAX_NAME) { 127 | try self.stream.reader().skipBytes(length, .{}); 128 | self.error_arg1 = length; 129 | return ServerError.InvalidFileName; 130 | } 131 | const count: usize = try self.stream.reader().readAll(buffer[0..length]); 132 | if (count < length) 133 | return ServerError.UnexpectedEndOfConnection; 134 | return buffer[0..count]; 135 | } 136 | fn write(self: *Self, buffer: []const u8) !void { 137 | try self.stream.writer().writeAll(buffer); 138 | } 139 | fn handlePing(self: *Self) !void { 140 | try self.send(.{ .PingReply = .{} }); 141 | } 142 | fn handleQuit(self: *Self) !void { 143 | self.isExiting = true; 144 | try self.send(.{ .QuitReply = .{} }); 145 | } 146 | fn handleCd(self: *Self, hdr: CdHeader) !void { 147 | var buf = [_]u8{0} ** MAX_NAME; 148 | const path = try self.readPath(hdr.length, buf[0..]); 149 | var depth: usize = 0; 150 | const dir = try self.openDir(path, &depth); 151 | self.cwd.close(); 152 | self.cwd = dir; 153 | self.depth = depth; 154 | try self.send(.{ .Ok = .{} }); 155 | } 156 | fn handlePwd(self: *Self) !void { 157 | var buf = [_]u8{0} ** MAX_PATH; 158 | var root_buf = [_]u8{0} ** MAX_PATH; 159 | const path = self.cwd.realpath(".", &buf) catch unreachable; 160 | const root_path = self.cwd_original.realpath(".", &root_buf) catch unreachable; 161 | try self.send( 162 | .{ 163 | .Path = .{ 164 | .length = @intCast(@max(path.len - root_path.len, 1)), 165 | }, 166 | }, 167 | ); 168 | if (path.len == root_path.len) 169 | try self.write("/"); 170 | try self.write(path[root_path.len..]); 171 | } 172 | fn handleList(self: *Self, hdr: ListHeader) !void { 173 | var buf = [_]u8{0} ** MAX_NAME; 174 | const path = try self.readPath(hdr.length, buf[0..]); 175 | var depth: usize = 0; 176 | var dir = try self.openDir(path, &depth); 177 | defer dir.close(); 178 | try self.send(.{ 179 | .Ok = .{}, 180 | }); 181 | var iterable = try dir.openIterableDir(".", .{}); 182 | defer iterable.close(); 183 | var iter = iterable.iterate(); 184 | while (try iter.next()) |entry| { 185 | switch (entry.kind) { 186 | .file, .directory => { 187 | try self.send( 188 | .{ 189 | .Entry = .{ 190 | .length = @intCast(entry.name.len), 191 | .is_dir = entry.kind == std.fs.File.Kind.directory, 192 | }, 193 | }, 194 | ); 195 | try self.write(entry.name); 196 | }, 197 | else => continue, 198 | } 199 | } 200 | try self.send(.{ .End = .{} }); 201 | } 202 | fn handleGetInfo(self: *Self) !void { 203 | try self.send( 204 | .{ 205 | .Info = .{ 206 | .max_name = MAX_NAME, 207 | .max_path = MAX_PATH, 208 | }, 209 | }, 210 | ); 211 | } 212 | fn copyDir(dir: std.fs.Dir) std.fs.Dir { 213 | return dir.openDir(".", .{}) catch unreachable; 214 | } 215 | fn openFile( 216 | self: *Self, 217 | path: []const u8, 218 | open_write: bool, 219 | parent: ?*std.fs.Dir, 220 | ) !std.fs.File { 221 | const file_name = std.fs.path.basename(path); 222 | if (file_name.len == 0) 223 | return ServerError.InvalidFileName; 224 | var dir = if (std.fs.path.dirname(path)) |dir_path| 225 | try self.openDir(dir_path, null) 226 | else 227 | copyDir(self.cwd); 228 | errdefer dir.close(); 229 | const file = (if (open_write) 230 | dir.createFile(file_name, .{}) 231 | else 232 | dir.openFile(file_name, .{})) catch |err| { 233 | return switch (err) { 234 | error.FileNotFound => ServerError.NonExisting, 235 | error.AccessDenied => ServerError.AccessDenied, 236 | error.IsDir => ServerError.IsNotFile, 237 | else => err, 238 | }; 239 | }; 240 | const stat = try file.stat(); 241 | switch (stat.kind) { 242 | .file => { 243 | if (parent) |prnt| { 244 | prnt.* = dir; 245 | } else { 246 | dir.close(); 247 | } 248 | return file; 249 | }, 250 | else => return ServerError.IsNotFile, 251 | } 252 | } 253 | fn nodeStat( 254 | self: *Self, 255 | path: []const u8, 256 | parent: ?*std.fs.Dir, 257 | ) !std.fs.File.Stat { 258 | const file_name = std.fs.path.basename(path); 259 | if (file_name.len == 0) 260 | return ServerError.InvalidFileName; 261 | var dir = if (std.fs.path.dirname(path)) |dir_path| 262 | try self.openDir(dir_path, null) 263 | else 264 | copyDir(self.cwd); 265 | errdefer dir.close(); 266 | const stt = dir.statFile(file_name) catch return ServerError.NonExisting; 267 | if (parent) |prnt| { 268 | prnt.* = dir; 269 | } else { 270 | dir.close(); 271 | } 272 | return stt; 273 | } 274 | fn handleRead(self: *Self, hdr: ReadHeader) !void { 275 | var buf = [_]u8{0} ** @max(MAX_NAME, READ_BUFFER_SIZE); 276 | const path = try self.readPath(hdr.length, buf[0..]); 277 | const file = try self.openFile( 278 | path, 279 | false, 280 | null, 281 | ); 282 | defer file.close(); 283 | const stat = try file.stat(); 284 | try self.send( 285 | .{ 286 | .File = .{ 287 | .size = stat.size, 288 | }, 289 | }, 290 | ); 291 | while (true) { 292 | const count = try file.readAll(buf[0..]); 293 | try self.write(buf[0..count]); 294 | if (count < buf.len) { 295 | break; 296 | } 297 | } 298 | } 299 | fn handleGetStat(self: *Self, hdr: PathHeader) !void { 300 | var buf = [_]u8{0} ** @max(MAX_NAME, READ_BUFFER_SIZE); 301 | const path = try self.readPath(hdr.length, buf[0..]); 302 | const stat = try self.nodeStat( 303 | path, 304 | null, 305 | ); 306 | const stat_resp: Header = switch (stat.kind) { 307 | .file => .{ 308 | .Stat = .{ .ty = NodeType.File, .size = stat.size }, 309 | }, 310 | .directory => .{ 311 | .Stat = .{ .ty = NodeType.Dir, .size = 0 }, 312 | }, 313 | else => return ServerError.NonExisting, 314 | }; 315 | try self.send(stat_resp); 316 | } 317 | fn handleDelete(self: *Self, hdr: DeleteHeader) !void { 318 | var buf = [_]u8{0} ** @max(MAX_NAME, READ_BUFFER_SIZE); 319 | const path = try self.readPath(hdr.length, buf[0..]); 320 | var parent: std.fs.Dir = undefined; 321 | const file = try self.openFile( 322 | path, 323 | true, 324 | &parent, 325 | ); 326 | defer file.close(); 327 | try parent.deleteFile(std.fs.path.basename(path)); 328 | try self.send(.{ .Ok = .{} }); 329 | } 330 | fn handleWrite(self: *Self, hdr: WriteHeader) !void { 331 | var buf = [_]u8{0} ** @max(MAX_NAME, READ_BUFFER_SIZE); 332 | const path = try self.readPath(hdr.length, buf[0..]); 333 | const file = try self.openFile( 334 | path, 335 | true, 336 | null, 337 | ); 338 | defer file.close(); 339 | try self.send(.{ .Ok = .{} }); 340 | const next_msg = try self.recv(); 341 | const fhdr = switch (next_msg.header) { 342 | .File => |fhdr| fhdr, 343 | else => { 344 | self.error_arg1 = @intFromEnum(next_msg.header); 345 | return ServerError.UnexpectedMessage; 346 | }, 347 | }; 348 | var rem = fhdr.size; 349 | while (rem > 0) { 350 | const count = try self.stream.reader().readAll(buf[0..@min(READ_BUFFER_SIZE, rem)]); 351 | rem -= count; 352 | _ = try file.write(buf[0..count]); 353 | } 354 | try self.send(.{ .Ok = .{} }); 355 | } 356 | fn handleMessage(self: *Self, mes: Message) !void { 357 | switch (mes.header) { 358 | .Ping => try self.handlePing(), 359 | .Quit => try self.handleQuit(), 360 | .Cd => |hdr| try self.handleCd(hdr), 361 | .Pwd => try self.handlePwd(), 362 | .List => |hdr| try self.handleList(hdr), 363 | .Read => |hdr| try self.handleRead(hdr), 364 | .Write => |hdr| try self.handleWrite(hdr), 365 | .GetInfo => try self.handleGetInfo(), 366 | .Delete => |hdr| try self.handleDelete(hdr), 367 | .GetStat => |hdr| try self.handleGetStat(hdr), 368 | .Corrupt => |hdr| { 369 | self.error_arg1 = hdr.tag; 370 | return ServerError.CorruptMessageTag; 371 | }, 372 | else => { 373 | self.error_arg1 = @intFromEnum(mes.header); 374 | return ServerError.UnexpectedMessage; 375 | }, 376 | } 377 | } 378 | 379 | pub fn handleClient(self: *Self) !void { 380 | while (!self.isExiting) { 381 | const mes = try self.recv(); 382 | self.handleMessage(mes) catch |err| { 383 | switch (err) { 384 | error.UnexpectedMessage, 385 | error.CorruptMessageTag, 386 | error.InvalidFileName, 387 | error.UnexpectedEndOfConnection, 388 | error.NonExisting, 389 | error.IsNotFile, 390 | error.IsNotDir, 391 | error.AccessDenied, 392 | error.CantOpen, 393 | => try self.sendError( 394 | @errSetCast(err), 395 | ), 396 | else => { 397 | return err; 398 | }, 399 | } 400 | self.error_arg1 = 0; 401 | self.error_arg2 = 0; 402 | }; 403 | } 404 | } 405 | }; 406 | } 407 | 408 | pub const Server = struct { 409 | const Self = @This(); 410 | const Handler = ServerHandler(net.Stream); 411 | 412 | config: Config, 413 | /// This is currently the server's bottleneck, as the number of TCP connections 414 | /// at any given time can't go above the thread count of the thread pool. 415 | pool: std.Thread.Pool, 416 | 417 | pub fn init(config: Config) Self { 418 | return .{ 419 | .config = config, 420 | .pool = undefined, 421 | }; 422 | } 423 | 424 | fn handlerThread(stream: net.Stream, config: Config) void { 425 | var stream_mut = stream; 426 | var handler = Handler.init(&config, &stream_mut); 427 | handler.handleClient() catch {}; 428 | } 429 | 430 | pub fn runServer(self: *Self) !void { 431 | try self.pool.init( 432 | .{ 433 | .allocator = self.config.allocator, 434 | .n_jobs = self.config.thread_pool_size, 435 | }, 436 | ); 437 | defer self.pool.deinit(); 438 | const addr = net.Address.resolveIp(self.config.address, self.config.port) catch { 439 | log.eprintln( 440 | "Invalid bind IP address", 441 | ); 442 | std.process.exit(1); 443 | }; 444 | var stream_server = net.StreamServer.init(.{}); 445 | stream_server.listen(addr) catch { 446 | log.errPrintFmt( 447 | "Could not listen on {s}:{d}\n", 448 | .{ 449 | self.config.address, 450 | self.config.port, 451 | }, 452 | ); 453 | std.process.exit(1); 454 | }; 455 | log.printFmt( 456 | "Server listening on {s}:{d}\n", 457 | .{ 458 | self.config.address, 459 | self.config.port, 460 | }, 461 | ); 462 | while (stream_server.accept()) |client| { 463 | try self.pool.spawn(handlerThread, .{ client.stream, self.config }); 464 | } else |_| { 465 | log.eprintln("Connection failure"); 466 | } 467 | } 468 | }; 469 | --------------------------------------------------------------------------------