╭ serve_common.zig ─────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("./event.zig"); 3 4 // the outcome of resolving a requested repo path to its on-disk directory. 5 // the http and ssh paths each map the cases to their own error responses. 6 pub const RepoPath = union(enum) { 7 ok: []const u8, // resolved on-disk path; the caller owns and frees it 8 invalid, // a <owner>/<repo> path was expected but not given 9 not_found, // unknown owner, or the repo doesn't exist and we aren't creating 10 }; 11 12 // resolve a requested repo path to its on-disk directory. in test mode the path 13 // is served directly; otherwise it's resolved as <owner>/<repo> through the 14 // event store, minting a repo event for a fresh push. 15 pub fn resolveRepoPath( 16 io: std.Io, 17 allocator: std.mem.Allocator, 18 is_test: bool, 19 repo_root_path: []const u8, 20 admin_repo_path: []const u8, 21 requested: []const u8, 22 create_if_missing: bool, 23 ) !RepoPath { 24 // the networking tests address repos by a trusted on-disk path (an absolute 25 // `requested` discards repo_root), so no traversal check is needed here 26 if (is_test) { 27 return .{ .ok = try std.fs.path.resolve(allocator, &.{ repo_root_path, requested }) }; 28 } 29 30 const owner_repo = evt.parseOwnerRepoPath(requested) orelse return .invalid; 31 const event_id_hex = (try evt.resolveOrCreateRepo(io, allocator, admin_repo_path, owner_repo.owner, owner_repo.name, create_if_missing)) orelse return .not_found; 32 return .{ .ok = try std.fs.path.join(allocator, &.{ repo_root_path, &event_id_hex }) }; 33 } 34 35 pub fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 36 err.print(fmt, args) catch return; 37 err.flush() catch {}; 38 } 39 40 // accept connections forever, spawning `handleConn(context, stream)` as a task 41 // for each. the handler owns the stream (it closes it). `name` labels accept 42 // errors in the log. shared by the http and ssh listeners. 43 pub fn runListener( 44 io: std.Io, 45 net_server: *std.Io.net.Server, 46 tasks: *std.Io.Group, 47 err: *std.Io.Writer, 48 name: []const u8, 49 context: anytype, 50 comptime handleConn: fn (@TypeOf(context), std.Io.net.Stream) void, 51 ) void { 52 const Context = @TypeOf(context); 53 54 const Conn = struct { 55 context: Context, 56 stream: std.Io.net.Stream, 57 58 fn run(c: @This()) void { 59 handleConn(c.context, c.stream); 60 } 61 }; 62 63 const Listener = struct { 64 io: std.Io, 65 net_server: *std.Io.net.Server, 66 tasks: *std.Io.Group, 67 err: *std.Io.Writer, 68 name: []const u8, 69 context: Context, 70 71 fn run(self: @This()) void { 72 while (true) { 73 const stream = self.net_server.accept(self.io) catch |accept_err| { 74 if (accept_err == error.Canceled) return; 75 logError(self.err, "{s} accept failed: {s}\n", .{ self.name, @errorName(accept_err) }); 76 continue; 77 }; 78 self.tasks.async(self.io, Conn.run, .{Conn{ .context = self.context, .stream = stream }}); 79 } 80 } 81 }; 82 83 tasks.async(io, Listener.run, .{Listener{ 84 .io = io, 85 .net_server = net_server, 86 .tasks = tasks, 87 .err = err, 88 .name = name, 89 .context = context, 90 }}); 91 } 92