authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-04 19:31:03-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-08-07 10:04:51-07:00
log9e5048c3a5be479d9ad72d1d004d385f2fdc8615
tree6e91dcaa9d2379e7282257fde261942948e1f68f
parentfef41c66dbe9e6ce3e9567b7d6d06ebf4d0cba06

fetch: update for new http API

it's not quite finished because I need to make it not copy the Resource

3 files changed, 311 insertions(+), 316 deletions(-)

lib/std/http/Client.zig+15-2
......@@ -682,7 +682,7 @@ pub const Response = struct {
682682 ///
683683 /// See also:
684684 /// * `readerDecompressing`
685 pub fn reader(response: *Response, buffer: []u8) *Reader {
685 pub fn reader(response: *const Response, buffer: []u8) *Reader {
686686 const req = response.request;
687687 if (!req.method.responseHasBody()) return .ending;
688688 const head = &response.head;
......@@ -805,6 +805,11 @@ pub const Request = struct {
805805 unhandled = std.math.maxInt(u16),
806806 _,
807807
808 pub fn init(n: u16) RedirectBehavior {
809 assert(n != std.math.maxInt(u16));
810 return @enumFromInt(n);
811 }
812
808813 pub fn subtractOne(rb: *RedirectBehavior) void {
809814 switch (rb.*) {
810815 .not_allowed => unreachable,
......@@ -855,6 +860,14 @@ pub const Request = struct {
855860 return result;
856861 }
857862
863 /// Transfers the HTTP head and body over the connection and flushes.
864 pub fn sendBodyComplete(r: *Request, body: []u8) Writer.Error!void {
865 r.transfer_encoding = .{ .content_length = body.len };
866 var bw = try sendBodyUnflushed(r, body);
867 bw.writer.end = body.len;
868 try bw.end();
869 }
870
858871 /// Transfers the HTTP head over the connection, which is not flushed until
859872 /// `BodyWriter.flush` or `BodyWriter.end` is called.
860873 ///
......@@ -1296,7 +1309,7 @@ pub const basic_authorization = struct {
12961309 pub fn value(uri: Uri, out: []u8) []u8 {
12971310 var bw: Writer = .fixed(out);
12981311 write(uri, &bw) catch unreachable;
1299 return bw.getWritten();
1312 return bw.buffered();
13001313 }
13011314
13021315 pub fn write(uri: Uri, out: *Writer) Writer.Error!void {
src/Package/Fetch.zig+140-183
......@@ -385,20 +385,21 @@ pub fn run(f: *Fetch) RunError!void {
385385 var resource: Resource = .{ .dir = dir };
386386 return f.runResource(path_or_url, &resource, null);
387387 } else |dir_err| {
388 var server_header_buffer: [init_resource_buffer_size]u8 = undefined;
389
388390 const file_err = if (dir_err == error.NotDir) e: {
389391 if (fs.cwd().openFile(path_or_url, .{})) |file| {
390 var resource: Resource = .{ .file = file };
392 var resource: Resource = .{ .file = file.reader(&server_header_buffer) };
391393 return f.runResource(path_or_url, &resource, null);
392394 } else |err| break :e err;
393395 } else dir_err;
394396
395397 const uri = std.Uri.parse(path_or_url) catch |uri_err| {
396398 return f.fail(0, try eb.printString(
397 "'{s}' could not be recognized as a file path ({s}) or an URL ({s})",
398 .{ path_or_url, @errorName(file_err), @errorName(uri_err) },
399 "'{s}' could not be recognized as a file path ({t}) or an URL ({t})",
400 .{ path_or_url, file_err, uri_err },
399401 ));
400402 };
401 var server_header_buffer: [header_buffer_size]u8 = undefined;
402403 var resource = try f.initResource(uri, &server_header_buffer);
403404 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null);
404405 }
......@@ -464,8 +465,8 @@ pub fn run(f: *Fetch) RunError!void {
464465 f.location_tok,
465466 try eb.printString("invalid URI: {s}", .{@errorName(err)}),
466467 );
467 var server_header_buffer: [header_buffer_size]u8 = undefined;
468 var resource = try f.initResource(uri, &server_header_buffer);
468 var buffer: [init_resource_buffer_size]u8 = undefined;
469 var resource = try f.initResource(uri, &buffer);
469470 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash);
470471}
471472
......@@ -866,8 +867,8 @@ fn fail(f: *Fetch, msg_tok: std.zig.Ast.TokenIndex, msg_str: u32) RunError {
866867}
867868
868869const Resource = union(enum) {
869 file: fs.File,
870 http_request: std.http.Client.Request,
870 file: fs.File.Reader,
871 http_request: HttpRequest,
871872 git: Git,
872873 dir: fs.Dir,
873874
......@@ -877,10 +878,16 @@ const Resource = union(enum) {
877878 want_oid: git.Oid,
878879 };
879880
881 const HttpRequest = struct {
882 request: std.http.Client.Request,
883 head: std.http.Client.Response.Head,
884 buffer: []u8,
885 };
886
880887 fn deinit(resource: *Resource) void {
881888 switch (resource.*) {
882 .file => |*file| file.close(),
883 .http_request => |*req| req.deinit(),
889 .file => |*file_reader| file_reader.file.close(),
890 .http_request => |*http_request| http_request.request.deinit(),
884891 .git => |*git_resource| {
885892 git_resource.fetch_stream.deinit();
886893 git_resource.session.deinit();
......@@ -890,21 +897,19 @@ const Resource = union(enum) {
890897 resource.* = undefined;
891898 }
892899
893 fn reader(resource: *Resource) std.io.AnyReader {
894 return .{
895 .context = resource,
896 .readFn = read,
897 };
898 }
899
900 fn read(context: *const anyopaque, buffer: []u8) anyerror!usize {
901 const resource: *Resource = @ptrCast(@alignCast(@constCast(context)));
902 switch (resource.*) {
903 .file => |*f| return f.read(buffer),
904 .http_request => |*r| return r.read(buffer),
905 .git => |*g| return g.fetch_stream.read(buffer),
900 fn reader(resource: *Resource) *std.Io.Reader {
901 return switch (resource.*) {
902 .file => |*file_reader| return &file_reader.interface,
903 .http_request => |*http_request| {
904 const response: std.http.Client.Response = .{
905 .request = &http_request.request,
906 .head = http_request.head,
907 };
908 return response.reader(http_request.buffer);
909 },
910 .git => |*g| return &g.fetch_stream.reader,
906911 .dir => unreachable,
907 }
912 };
908913 }
909914};
910915
......@@ -967,20 +972,21 @@ const FileType = enum {
967972 }
968973};
969974
970const header_buffer_size = 16 * 1024;
975const init_resource_buffer_size = git.Packet.max_data_length;
971976
972fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Resource {
977fn initResource(f: *Fetch, uri: std.Uri, reader_buffer: []u8) RunError!Resource {
973978 const gpa = f.arena.child_allocator;
974979 const arena = f.arena.allocator();
975980 const eb = &f.error_bundle;
976981
977982 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
978983 const path = try uri.path.toRawMaybeAlloc(arena);
979 return .{ .file = f.parent_package_root.openFile(path, .{}) catch |err| {
980 return f.fail(f.location_tok, try eb.printString("unable to open '{f}{s}': {s}", .{
981 f.parent_package_root, path, @errorName(err),
984 const file = f.parent_package_root.openFile(path, .{}) catch |err| {
985 return f.fail(f.location_tok, try eb.printString("unable to open '{f}{s}': {t}", .{
986 f.parent_package_root, path, err,
982987 }));
983 } };
988 };
989 return .{ .file = file.reader(reader_buffer) };
984990 }
985991
986992 const http_client = f.job_queue.http_client;
......@@ -988,37 +994,27 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
988994 if (ascii.eqlIgnoreCase(uri.scheme, "http") or
989995 ascii.eqlIgnoreCase(uri.scheme, "https"))
990996 {
991 var req = http_client.open(.GET, uri, .{
992 .server_header_buffer = server_header_buffer,
993 }) catch |err| {
994 return f.fail(f.location_tok, try eb.printString(
995 "unable to connect to server: {s}",
996 .{@errorName(err)},
997 ));
998 };
999 errdefer req.deinit(); // releases more than memory
997 var request = http_client.request(.GET, uri, .{}) catch |err|
998 return f.fail(f.location_tok, try eb.printString("unable to connect to server: {t}", .{err}));
999 defer request.deinit();
10001000
1001 req.send() catch |err| {
1002 return f.fail(f.location_tok, try eb.printString(
1003 "HTTP request failed: {s}",
1004 .{@errorName(err)},
1005 ));
1006 };
1007 req.wait() catch |err| {
1008 return f.fail(f.location_tok, try eb.printString(
1009 "invalid HTTP response: {s}",
1010 .{@errorName(err)},
1011 ));
1012 };
1001 request.sendBodiless() catch |err|
1002 return f.fail(f.location_tok, try eb.printString("HTTP request failed: {t}", .{err}));
10131003
1014 if (req.response.status != .ok) {
1015 return f.fail(f.location_tok, try eb.printString(
1016 "bad HTTP response code: '{d} {s}'",
1017 .{ @intFromEnum(req.response.status), req.response.status.phrase() orelse "" },
1018 ));
1019 }
1004 var redirect_buffer: [1024]u8 = undefined;
1005 const response = request.receiveHead(&redirect_buffer) catch |err|
1006 return f.fail(f.location_tok, try eb.printString("invalid HTTP response: {t}", .{err}));
1007
1008 if (response.head.status != .ok) return f.fail(f.location_tok, try eb.printString(
1009 "bad HTTP response code: '{d} {s}'",
1010 .{ response.head.status, response.head.status.phrase() orelse "" },
1011 ));
10201012
1021 return .{ .http_request = req };
1013 return .{ .http_request = .{
1014 .request = request,
1015 .head = response.head,
1016 .buffer = reader_buffer,
1017 } };
10221018 }
10231019
10241020 if (ascii.eqlIgnoreCase(uri.scheme, "git+http") or
......@@ -1026,7 +1022,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
10261022 {
10271023 var transport_uri = uri;
10281024 transport_uri.scheme = uri.scheme["git+".len..];
1029 var session = git.Session.init(gpa, http_client, transport_uri, server_header_buffer) catch |err| {
1025 var session = git.Session.init(gpa, http_client, transport_uri, reader_buffer) catch |err| {
10301026 return f.fail(f.location_tok, try eb.printString(
10311027 "unable to discover remote git server capabilities: {s}",
10321028 .{@errorName(err)},
......@@ -1042,16 +1038,12 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
10421038 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
10431039 const want_ref_tag = try std.fmt.allocPrint(arena, "refs/tags/{s}", .{want_ref});
10441040
1045 var ref_iterator = session.listRefs(.{
1041 var ref_iterator: git.Session.RefIterator = undefined;
1042 session.listRefs(&ref_iterator, .{
10461043 .ref_prefixes = &.{ want_ref, want_ref_head, want_ref_tag },
10471044 .include_peeled = true,
1048 .server_header_buffer = server_header_buffer,
1049 }) catch |err| {
1050 return f.fail(f.location_tok, try eb.printString(
1051 "unable to list refs: {s}",
1052 .{@errorName(err)},
1053 ));
1054 };
1045 .buffer = reader_buffer,
1046 }) catch |err| return f.fail(f.location_tok, try eb.printString("unable to list refs: {t}", .{err}));
10551047 defer ref_iterator.deinit();
10561048 while (ref_iterator.next() catch |err| {
10571049 return f.fail(f.location_tok, try eb.printString(
......@@ -1089,14 +1081,14 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
10891081
10901082 var want_oid_buf: [git.Oid.max_formatted_length]u8 = undefined;
10911083 _ = std.fmt.bufPrint(&want_oid_buf, "{f}", .{want_oid}) catch unreachable;
1092 var fetch_stream = session.fetch(&.{&want_oid_buf}, server_header_buffer) catch |err| {
1093 return f.fail(f.location_tok, try eb.printString(
1094 "unable to create fetch stream: {s}",
1095 .{@errorName(err)},
1096 ));
1084 var fetch_stream: git.Session.FetchStream = undefined;
1085 session.fetch(&fetch_stream, &.{&want_oid_buf}, reader_buffer) catch |err| {
1086 return f.fail(f.location_tok, try eb.printString("unable to create fetch stream: {t}", .{err}));
10971087 };
10981088 errdefer fetch_stream.deinit();
10991089
1090 if (true) @panic("TODO this moves fetch_stream, invalidating its reader");
1091
11001092 return .{ .git = .{
11011093 .session = session,
11021094 .fetch_stream = fetch_stream,
......@@ -1104,10 +1096,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
11041096 } };
11051097 }
11061098
1107 return f.fail(f.location_tok, try eb.printString(
1108 "unsupported URL scheme: {s}",
1109 .{uri.scheme},
1110 ));
1099 return f.fail(f.location_tok, try eb.printString("unsupported URL scheme: {s}", .{uri.scheme}));
11111100}
11121101
11131102fn unpackResource(
......@@ -1121,9 +1110,11 @@ fn unpackResource(
11211110 .file => FileType.fromPath(uri_path) orelse
11221111 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path})),
11231112
1124 .http_request => |req| ft: {
1113 .http_request => |*http_request| ft: {
1114 const head = &http_request.head;
1115
11251116 // Content-Type takes first precedence.
1126 const content_type = req.response.content_type orelse
1117 const content_type = head.content_type orelse
11271118 return f.fail(f.location_tok, try eb.addString("missing 'Content-Type' header"));
11281119
11291120 // Extract the MIME type, ignoring charset and boundary directives
......@@ -1165,7 +1156,7 @@ fn unpackResource(
11651156 }
11661157
11671158 // Next, the filename from 'content-disposition: attachment' takes precedence.
1168 if (req.response.content_disposition) |cd_header| {
1159 if (head.content_disposition) |cd_header| {
11691160 break :ft FileType.fromContentDisposition(cd_header) orelse {
11701161 return f.fail(f.location_tok, try eb.printString(
11711162 "unsupported Content-Disposition header value: '{s}' for Content-Type=application/octet-stream",
......@@ -1176,10 +1167,7 @@ fn unpackResource(
11761167
11771168 // Finally, the path from the URI is used.
11781169 break :ft FileType.fromPath(uri_path) orelse {
1179 return f.fail(f.location_tok, try eb.printString(
1180 "unknown file type: '{s}'",
1181 .{uri_path},
1182 ));
1170 return f.fail(f.location_tok, try eb.printString("unknown file type: '{s}'", .{uri_path}));
11831171 };
11841172 },
11851173
......@@ -1187,10 +1175,9 @@ fn unpackResource(
11871175
11881176 .dir => |dir| {
11891177 f.recursiveDirectoryCopy(dir, tmp_directory.handle) catch |err| {
1190 return f.fail(f.location_tok, try eb.printString(
1191 "unable to copy directory '{s}': {s}",
1192 .{ uri_path, @errorName(err) },
1193 ));
1178 return f.fail(f.location_tok, try eb.printString("unable to copy directory '{s}': {t}", .{
1179 uri_path, err,
1180 }));
11941181 };
11951182 return .{};
11961183 },
......@@ -1198,15 +1185,11 @@ fn unpackResource(
11981185
11991186 switch (file_type) {
12001187 .tar => {
1201 var adapter_buffer: [1024]u8 = undefined;
1202 var adapter = resource.reader().adaptToNewApi(&adapter_buffer);
1203 return unpackTarball(f, tmp_directory.handle, &adapter.new_interface);
1188 return unpackTarball(f, tmp_directory.handle, resource.reader());
12041189 },
12051190 .@"tar.gz" => {
1206 var adapter_buffer: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined;
1207 var adapter = resource.reader().adaptToNewApi(&adapter_buffer);
12081191 var flate_buffer: [std.compress.flate.max_window_len]u8 = undefined;
1209 var decompress: std.compress.flate.Decompress = .init(&adapter.new_interface, .gzip, &flate_buffer);
1192 var decompress: std.compress.flate.Decompress = .init(resource.reader(), .gzip, &flate_buffer);
12101193 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
12111194 },
12121195 .@"tar.xz" => {
......@@ -1227,9 +1210,7 @@ fn unpackResource(
12271210 .@"tar.zst" => {
12281211 const window_size = std.compress.zstd.default_window_len;
12291212 const window_buffer = try f.arena.allocator().create([window_size]u8);
1230 var adapter_buffer: [std.crypto.tls.max_ciphertext_record_len]u8 = undefined;
1231 var adapter = resource.reader().adaptToNewApi(&adapter_buffer);
1232 var decompress: std.compress.zstd.Decompress = .init(&adapter.new_interface, window_buffer, .{
1213 var decompress: std.compress.zstd.Decompress = .init(resource.reader(), window_buffer, .{
12331214 .verify_checksum = false,
12341215 });
12351216 return try unpackTarball(f, tmp_directory.handle, &decompress.reader);
......@@ -1237,12 +1218,15 @@ fn unpackResource(
12371218 .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) {
12381219 error.FetchFailed => return error.FetchFailed,
12391220 error.OutOfMemory => return error.OutOfMemory,
1240 else => |e| return f.fail(f.location_tok, try eb.printString(
1241 "unable to unpack git files: {s}",
1242 .{@errorName(e)},
1221 else => |e| return f.fail(f.location_tok, try eb.printString("unable to unpack git files: {t}", .{e})),
1222 },
1223 .zip => return unzip(f, tmp_directory.handle, resource.reader()) catch |err| switch (err) {
1224 error.ReadFailed => return f.fail(f.location_tok, try eb.printString(
1225 "failed reading resource: {t}",
1226 .{err},
12431227 )),
1228 else => |e| return e,
12441229 },
1245 .zip => return try unzip(f, tmp_directory.handle, resource.reader()),
12461230 }
12471231}
12481232
......@@ -1277,99 +1261,69 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!Un
12771261 return res;
12781262}
12791263
1280fn unzip(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult {
1264fn unzip(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) error{ ReadFailed, OutOfMemory, FetchFailed }!UnpackResult {
12811265 // We write the entire contents to a file first because zip files
12821266 // must be processed back to front and they could be too large to
12831267 // load into memory.
12841268
12851269 const cache_root = f.job_queue.global_cache;
1286
1287 // TODO: the downside of this solution is if we get a failure/crash/oom/power out
1288 // during this process, we leave behind a zip file that would be
1289 // difficult to know if/when it can be cleaned up.
1290 // Might be worth it to use a mechanism that enables other processes
1291 // to see if the owning process of a file is still alive (on linux this
1292 // can be done with file locks).
1293 // Coupled with this mechansism, we could also use slots (i.e. zig-cache/tmp/0,
1294 // zig-cache/tmp/1, etc) which would mean that subsequent runs would
1295 // automatically clean up old dead files.
1296 // This could all be done with a simple TmpFile abstraction.
12971270 const prefix = "tmp/";
12981271 const suffix = ".zip";
1299
1300 const random_bytes_count = 20;
1301 const random_path_len = comptime std.fs.base64_encoder.calcSize(random_bytes_count);
1302 var zip_path: [prefix.len + random_path_len + suffix.len]u8 = undefined;
1303 @memcpy(zip_path[0..prefix.len], prefix);
1304 @memcpy(zip_path[prefix.len + random_path_len ..], suffix);
1305 {
1306 var random_bytes: [random_bytes_count]u8 = undefined;
1307 std.crypto.random.bytes(&random_bytes);
1308 _ = std.fs.base64_encoder.encode(
1309 zip_path[prefix.len..][0..random_path_len],
1310 &random_bytes,
1311 );
1312 }
1313
1314 defer cache_root.handle.deleteFile(&zip_path) catch {};
1315
13161272 const eb = &f.error_bundle;
1317
1318 {
1319 var zip_file = cache_root.handle.createFile(
1320 &zip_path,
1321 .{},
1322 ) catch |err| return f.fail(f.location_tok, try eb.printString(
1323 "failed to create tmp zip file: {s}",
1324 .{@errorName(err)},
1325 ));
1326 defer zip_file.close();
1327 var buf: [4096]u8 = undefined;
1328 while (true) {
1329 const len = reader.readAll(&buf) catch |err| return f.fail(f.location_tok, try eb.printString(
1330 "read zip stream failed: {s}",
1331 .{@errorName(err)},
1332 ));
1333 if (len == 0) break;
1334 zip_file.deprecatedWriter().writeAll(buf[0..len]) catch |err| return f.fail(f.location_tok, try eb.printString(
1335 "write temporary zip file failed: {s}",
1336 .{@errorName(err)},
1337 ));
1338 }
1339 }
1273 const random_len = @sizeOf(u64) * 2;
1274
1275 var zip_path: [prefix.len + random_len + suffix.len]u8 = undefined;
1276 zip_path[0..prefix.len].* = prefix.*;
1277 zip_path[prefix.len + random_len ..].* = suffix.*;
1278
1279 var zip_file = while (true) {
1280 const random_integer = std.crypto.random.int(u64);
1281 zip_path[prefix.len..][0..random_len].* = std.fmt.hex(random_integer);
1282
1283 break cache_root.handle.createFile(&zip_path, .{
1284 .exclusive = true,
1285 .read = true,
1286 }) catch |err| switch (err) {
1287 error.PathAlreadyExists => continue,
1288 else => |e| return f.fail(
1289 f.location_tok,
1290 try eb.printString("failed to create temporary zip file: {t}", .{e}),
1291 ),
1292 };
1293 };
1294 defer zip_file.close();
1295 var zip_file_buffer: [4096]u8 = undefined;
1296 var zip_file_reader = b: {
1297 var zip_file_writer = zip_file.writer(&zip_file_buffer);
1298
1299 _ = reader.streamRemaining(&zip_file_writer.interface) catch |err| switch (err) {
1300 error.ReadFailed => return error.ReadFailed,
1301 error.WriteFailed => return f.fail(
1302 f.location_tok,
1303 try eb.printString("failed writing temporary zip file: {t}", .{err}),
1304 ),
1305 };
1306 zip_file_writer.interface.flush() catch |err| return f.fail(
1307 f.location_tok,
1308 try eb.printString("failed writing temporary zip file: {t}", .{err}),
1309 );
1310 break :b zip_file_writer.moveToReader();
1311 };
13401312
13411313 var diagnostics: std.zip.Diagnostics = .{ .allocator = f.arena.allocator() };
13421314 // no need to deinit since we are using an arena allocator
13431315
1344 {
1345 var zip_file = cache_root.handle.openFile(
1346 &zip_path,
1347 .{},
1348 ) catch |err| return f.fail(f.location_tok, try eb.printString(
1349 "failed to open temporary zip file: {s}",
1350 .{@errorName(err)},
1351 ));
1352 defer zip_file.close();
1353
1354 var zip_file_buffer: [1024]u8 = undefined;
1355 var zip_file_reader = zip_file.reader(&zip_file_buffer);
1356
1357 std.zip.extract(out_dir, &zip_file_reader, .{
1358 .allow_backslashes = true,
1359 .diagnostics = &diagnostics,
1360 }) catch |err| return f.fail(f.location_tok, try eb.printString(
1361 "zip extract failed: {s}",
1362 .{@errorName(err)},
1363 ));
1364 }
1316 zip_file_reader.seekTo(0) catch |err|
1317 return f.fail(f.location_tok, try eb.printString("failed to seek temporary zip file: {t}", .{err}));
1318 std.zip.extract(out_dir, &zip_file_reader, .{
1319 .allow_backslashes = true,
1320 .diagnostics = &diagnostics,
1321 }) catch |err| return f.fail(f.location_tok, try eb.printString("zip extract failed: {t}", .{err}));
13651322
1366 cache_root.handle.deleteFile(&zip_path) catch |err| return f.fail(f.location_tok, try eb.printString(
1367 "delete temporary zip failed: {s}",
1368 .{@errorName(err)},
1369 ));
1323 cache_root.handle.deleteFile(&zip_path) catch |err|
1324 return f.fail(f.location_tok, try eb.printString("delete temporary zip failed: {t}", .{err}));
13701325
1371 const res: UnpackResult = .{ .root_dir = diagnostics.root_dir };
1372 return res;
1326 return .{ .root_dir = diagnostics.root_dir };
13731327}
13741328
13751329fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!UnpackResult {
......@@ -1387,10 +1341,13 @@ fn unpackGitPack(f: *Fetch, out_dir: fs.Dir, resource: *Resource.Git) anyerror!U
13871341 var pack_file = try pack_dir.createFile("pkg.pack", .{ .read = true });
13881342 defer pack_file.close();
13891343 var pack_file_buffer: [4096]u8 = undefined;
1390 var fifo = std.fifo.LinearFifo(u8, .{ .Slice = {} }).init(&pack_file_buffer);
1391 try fifo.pump(resource.fetch_stream.reader(), pack_file.deprecatedWriter());
1392
1393 var pack_file_reader = pack_file.reader(&pack_file_buffer);
1344 var pack_file_reader = b: {
1345 var pack_file_writer = pack_file.writer(&pack_file_buffer);
1346 const fetch_reader = &resource.fetch_stream.reader;
1347 _ = try fetch_reader.streamRemaining(&pack_file_writer.interface);
1348 try pack_file_writer.interface.flush();
1349 break :b pack_file_writer.moveToReader();
1350 };
13941351
13951352 var index_file = try pack_dir.createFile("pkg.idx", .{ .read = true });
13961353 defer index_file.close();
src/Package/Fetch/git.zig+156-131
......@@ -585,17 +585,17 @@ const ObjectCache = struct {
585585/// [protocol-common](https://git-scm.com/docs/protocol-common). The special
586586/// meanings of the delimiter and response-end packets are documented in
587587/// [protocol-v2](https://git-scm.com/docs/protocol-v2).
588const Packet = union(enum) {
588pub const Packet = union(enum) {
589589 flush,
590590 delimiter,
591591 response_end,
592592 data: []const u8,
593593
594 const max_data_length = 65516;
594 pub const max_data_length = 65516;
595595
596596 /// Reads a packet in pkt-line format.
597 fn read(reader: anytype, buf: *[max_data_length]u8) !Packet {
598 const length = std.fmt.parseUnsigned(u16, &try reader.readBytesNoEof(4), 16) catch return error.InvalidPacket;
597 fn read(reader: *std.Io.Reader) !Packet {
598 const length = std.fmt.parseUnsigned(u16, try reader.take(4), 16) catch return error.InvalidPacket;
599599 switch (length) {
600600 0 => return .flush,
601601 1 => return .delimiter,
......@@ -603,13 +603,11 @@ const Packet = union(enum) {
603603 3 => return error.InvalidPacket,
604604 else => if (length - 4 > max_data_length) return error.InvalidPacket,
605605 }
606 const data = buf[0 .. length - 4];
607 try reader.readNoEof(data);
608 return .{ .data = data };
606 return .{ .data = try reader.take(length - 4) };
609607 }
610608
611609 /// Writes a packet in pkt-line format.
612 fn write(packet: Packet, writer: anytype) !void {
610 fn write(packet: Packet, writer: *std.Io.Writer) !void {
613611 switch (packet) {
614612 .flush => try writer.writeAll("0000"),
615613 .delimiter => try writer.writeAll("0001"),
......@@ -657,8 +655,10 @@ pub const Session = struct {
657655 allocator: Allocator,
658656 transport: *std.http.Client,
659657 uri: std.Uri,
660 http_headers_buffer: []u8,
658 /// Asserted to be at least `Packet.max_data_length`
659 response_buffer: []u8,
661660 ) !Session {
661 assert(response_buffer.len >= Packet.max_data_length);
662662 var session: Session = .{
663663 .transport = transport,
664664 .location = try .init(allocator, uri),
......@@ -668,7 +668,8 @@ pub const Session = struct {
668668 .allocator = allocator,
669669 };
670670 errdefer session.deinit();
671 var capability_iterator = try session.getCapabilities(http_headers_buffer);
671 var capability_iterator: CapabilityIterator = undefined;
672 try session.getCapabilities(&capability_iterator, response_buffer);
672673 defer capability_iterator.deinit();
673674 while (try capability_iterator.next()) |capability| {
674675 if (mem.eql(u8, capability.key, "agent")) {
......@@ -743,7 +744,8 @@ pub const Session = struct {
743744 ///
744745 /// The `session.location` is updated if the server returns a redirect, so
745746 /// that subsequent session functions do not need to handle redirects.
746 fn getCapabilities(session: *Session, http_headers_buffer: []u8) !CapabilityIterator {
747 fn getCapabilities(session: *Session, it: *CapabilityIterator, response_buffer: []u8) !void {
748 assert(response_buffer.len >= Packet.max_data_length);
747749 var info_refs_uri = session.location.uri;
748750 {
749751 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{
......@@ -757,19 +759,22 @@ pub const Session = struct {
757759 info_refs_uri.fragment = null;
758760
759761 const max_redirects = 3;
760 var request = try session.transport.open(.GET, info_refs_uri, .{
761 .redirect_behavior = @enumFromInt(max_redirects),
762 .server_header_buffer = http_headers_buffer,
763 .extra_headers = &.{
764 .{ .name = "Git-Protocol", .value = "version=2" },
765 },
766 });
767 errdefer request.deinit();
768 try request.send();
769 try request.finish();
762 it.* = .{
763 .request = try session.transport.request(.GET, info_refs_uri, .{
764 .redirect_behavior = .init(max_redirects),
765 .extra_headers = &.{
766 .{ .name = "Git-Protocol", .value = "version=2" },
767 },
768 }),
769 .reader = undefined,
770 };
771 errdefer it.deinit();
772 const request = &it.request;
773 try request.sendBodiless();
770774
771 try request.wait();
772 if (request.response.status != .ok) return error.ProtocolError;
775 var redirect_buffer: [1024]u8 = undefined;
776 const response = try request.receiveHead(&redirect_buffer);
777 if (response.head.status != .ok) return error.ProtocolError;
773778 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
774779 if (any_redirects_occurred) {
775780 const request_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{
......@@ -784,8 +789,7 @@ pub const Session = struct {
784789 session.location = new_location;
785790 }
786791
787 const reader = request.reader();
788 var buf: [Packet.max_data_length]u8 = undefined;
792 it.reader = response.reader(response_buffer);
789793 var state: enum { response_start, response_content } = .response_start;
790794 while (true) {
791795 // Some Git servers (at least GitHub) include an additional
......@@ -795,15 +799,15 @@ pub const Session = struct {
795799 // Thus, we need to skip any such useless additional responses
796800 // before we get the one we're actually looking for. The responses
797801 // will be delimited by flush packets.
798 const packet = Packet.read(reader, &buf) catch |e| switch (e) {
802 const packet = Packet.read(it.reader) catch |err| switch (err) {
799803 error.EndOfStream => return error.UnsupportedProtocol, // 'version 2' packet not found
800 else => |other| return other,
804 else => |e| return e,
801805 };
802806 switch (packet) {
803807 .flush => state = .response_start,
804808 .data => |data| switch (state) {
805809 .response_start => if (mem.eql(u8, Packet.normalizeText(data), "version 2")) {
806 return .{ .request = request };
810 return;
807811 } else {
808812 state = .response_content;
809813 },
......@@ -816,7 +820,7 @@ pub const Session = struct {
816820
817821 const CapabilityIterator = struct {
818822 request: std.http.Client.Request,
819 buf: [Packet.max_data_length]u8 = undefined,
823 reader: *std.Io.Reader,
820824
821825 const Capability = struct {
822826 key: []const u8,
......@@ -830,13 +834,13 @@ pub const Session = struct {
830834 }
831835 };
832836
833 fn deinit(iterator: *CapabilityIterator) void {
834 iterator.request.deinit();
835 iterator.* = undefined;
837 fn deinit(it: *CapabilityIterator) void {
838 it.request.deinit();
839 it.* = undefined;
836840 }
837841
838 fn next(iterator: *CapabilityIterator) !?Capability {
839 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
842 fn next(it: *CapabilityIterator) !?Capability {
843 switch (try Packet.read(it.reader)) {
840844 .flush => return null,
841845 .data => |data| return Capability.parse(Packet.normalizeText(data)),
842846 else => return error.UnexpectedPacket,
......@@ -854,11 +858,13 @@ pub const Session = struct {
854858 include_symrefs: bool = false,
855859 /// Whether to include the peeled object ID for returned tag refs.
856860 include_peeled: bool = false,
857 server_header_buffer: []u8,
861 /// Asserted to be at least `Packet.max_data_length`.
862 buffer: []u8,
858863 };
859864
860865 /// Returns an iterator over refs known to the server.
861 pub fn listRefs(session: Session, options: ListRefsOptions) !RefIterator {
866 pub fn listRefs(session: Session, it: *RefIterator, options: ListRefsOptions) !void {
867 assert(options.buffer.len >= Packet.max_data_length);
862868 var upload_pack_uri = session.location.uri;
863869 {
864870 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{
......@@ -871,59 +877,56 @@ pub const Session = struct {
871877 upload_pack_uri.query = null;
872878 upload_pack_uri.fragment = null;
873879
874 var body: std.ArrayListUnmanaged(u8) = .empty;
875 defer body.deinit(session.allocator);
876 const body_writer = body.writer(session.allocator);
877 try Packet.write(.{ .data = "command=ls-refs\n" }, body_writer);
880 var body: std.Io.Writer = .fixed(options.buffer);
881 try Packet.write(.{ .data = "command=ls-refs\n" }, &body);
878882 if (session.supports_agent) {
879 try Packet.write(.{ .data = agent_capability }, body_writer);
883 try Packet.write(.{ .data = agent_capability }, &body);
880884 }
881885 {
882 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
886 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={t}\n", .{
887 session.object_format,
888 });
883889 defer session.allocator.free(object_format_packet);
884 try Packet.write(.{ .data = object_format_packet }, body_writer);
890 try Packet.write(.{ .data = object_format_packet }, &body);
885891 }
886 try Packet.write(.delimiter, body_writer);
892 try Packet.write(.delimiter, &body);
887893 for (options.ref_prefixes) |ref_prefix| {
888894 const ref_prefix_packet = try std.fmt.allocPrint(session.allocator, "ref-prefix {s}\n", .{ref_prefix});
889895 defer session.allocator.free(ref_prefix_packet);
890 try Packet.write(.{ .data = ref_prefix_packet }, body_writer);
896 try Packet.write(.{ .data = ref_prefix_packet }, &body);
891897 }
892898 if (options.include_symrefs) {
893 try Packet.write(.{ .data = "symrefs\n" }, body_writer);
899 try Packet.write(.{ .data = "symrefs\n" }, &body);
894900 }
895901 if (options.include_peeled) {
896 try Packet.write(.{ .data = "peel\n" }, body_writer);
902 try Packet.write(.{ .data = "peel\n" }, &body);
897903 }
898 try Packet.write(.flush, body_writer);
899
900 var request = try session.transport.open(.POST, upload_pack_uri, .{
901 .redirect_behavior = .unhandled,
902 .server_header_buffer = options.server_header_buffer,
903 .extra_headers = &.{
904 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
905 .{ .name = "Git-Protocol", .value = "version=2" },
906 },
907 });
908 errdefer request.deinit();
909 request.transfer_encoding = .{ .content_length = body.items.len };
910 try request.send();
911 try request.writeAll(body.items);
912 try request.finish();
913
914 try request.wait();
915 if (request.response.status != .ok) return error.ProtocolError;
916
917 return .{
904 try Packet.write(.flush, &body);
905
906 it.* = .{
907 .request = try session.transport.request(.POST, upload_pack_uri, .{
908 .redirect_behavior = .unhandled,
909 .extra_headers = &.{
910 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
911 .{ .name = "Git-Protocol", .value = "version=2" },
912 },
913 }),
914 .reader = undefined,
918915 .format = session.object_format,
919 .request = request,
920916 };
917 const request = &it.request;
918 errdefer request.deinit();
919 try request.sendBodyComplete(body.buffered());
920
921 const response = try request.receiveHead(options.buffer);
922 if (response.head.status != .ok) return error.ProtocolError;
923 it.reader = response.reader(options.buffer);
921924 }
922925
923926 pub const RefIterator = struct {
924927 format: Oid.Format,
925928 request: std.http.Client.Request,
926 buf: [Packet.max_data_length]u8 = undefined,
929 reader: *std.Io.Reader,
927930
928931 pub const Ref = struct {
929932 oid: Oid,
......@@ -937,13 +940,13 @@ pub const Session = struct {
937940 iterator.* = undefined;
938941 }
939942
940 pub fn next(iterator: *RefIterator) !?Ref {
941 switch (try Packet.read(iterator.request.reader(), &iterator.buf)) {
943 pub fn next(it: *RefIterator) !?Ref {
944 switch (try Packet.read(it.reader)) {
942945 .flush => return null,
943946 .data => |data| {
944947 const ref_data = Packet.normalizeText(data);
945948 const oid_sep_pos = mem.indexOfScalar(u8, ref_data, ' ') orelse return error.InvalidRefPacket;
946 const oid = Oid.parse(iterator.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
949 const oid = Oid.parse(it.format, data[0..oid_sep_pos]) catch return error.InvalidRefPacket;
947950
948951 const name_sep_pos = mem.indexOfScalarPos(u8, ref_data, oid_sep_pos + 1, ' ') orelse ref_data.len;
949952 const name = ref_data[oid_sep_pos + 1 .. name_sep_pos];
......@@ -957,7 +960,7 @@ pub const Session = struct {
957960 if (mem.startsWith(u8, attribute, "symref-target:")) {
958961 symref_target = attribute["symref-target:".len..];
959962 } else if (mem.startsWith(u8, attribute, "peeled:")) {
960 peeled = Oid.parse(iterator.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
963 peeled = Oid.parse(it.format, attribute["peeled:".len..]) catch return error.InvalidRefPacket;
961964 }
962965 last_sep_pos = next_sep_pos;
963966 }
......@@ -973,9 +976,12 @@ pub const Session = struct {
973976 /// performed if the server supports it.
974977 pub fn fetch(
975978 session: Session,
979 fs: *FetchStream,
976980 wants: []const []const u8,
977 http_headers_buffer: []u8,
978 ) !FetchStream {
981 /// Asserted to be at least `Packet.max_data_length`.
982 response_buffer: []u8,
983 ) !void {
984 assert(response_buffer.len >= Packet.max_data_length);
979985 var upload_pack_uri = session.location.uri;
980986 {
981987 const session_uri_path = try std.fmt.allocPrint(session.allocator, "{f}", .{
......@@ -988,63 +994,71 @@ pub const Session = struct {
988994 upload_pack_uri.query = null;
989995 upload_pack_uri.fragment = null;
990996
991 var body: std.ArrayListUnmanaged(u8) = .empty;
992 defer body.deinit(session.allocator);
993 const body_writer = body.writer(session.allocator);
994 try Packet.write(.{ .data = "command=fetch\n" }, body_writer);
997 var body: std.Io.Writer = .fixed(response_buffer);
998 try Packet.write(.{ .data = "command=fetch\n" }, &body);
995999 if (session.supports_agent) {
996 try Packet.write(.{ .data = agent_capability }, body_writer);
1000 try Packet.write(.{ .data = agent_capability }, &body);
9971001 }
9981002 {
9991003 const object_format_packet = try std.fmt.allocPrint(session.allocator, "object-format={s}\n", .{@tagName(session.object_format)});
10001004 defer session.allocator.free(object_format_packet);
1001 try Packet.write(.{ .data = object_format_packet }, body_writer);
1005 try Packet.write(.{ .data = object_format_packet }, &body);
10021006 }
1003 try Packet.write(.delimiter, body_writer);
1007 try Packet.write(.delimiter, &body);
10041008 // Our packfile parser supports the OFS_DELTA object type
1005 try Packet.write(.{ .data = "ofs-delta\n" }, body_writer);
1009 try Packet.write(.{ .data = "ofs-delta\n" }, &body);
10061010 // We do not currently convey server progress information to the user
1007 try Packet.write(.{ .data = "no-progress\n" }, body_writer);
1011 try Packet.write(.{ .data = "no-progress\n" }, &body);
10081012 if (session.supports_shallow) {
1009 try Packet.write(.{ .data = "deepen 1\n" }, body_writer);
1013 try Packet.write(.{ .data = "deepen 1\n" }, &body);
10101014 }
10111015 for (wants) |want| {
10121016 var buf: [Packet.max_data_length]u8 = undefined;
10131017 const arg = std.fmt.bufPrint(&buf, "want {s}\n", .{want}) catch unreachable;
1014 try Packet.write(.{ .data = arg }, body_writer);
1018 try Packet.write(.{ .data = arg }, &body);
10151019 }
1016 try Packet.write(.{ .data = "done\n" }, body_writer);
1017 try Packet.write(.flush, body_writer);
1018
1019 var request = try session.transport.open(.POST, upload_pack_uri, .{
1020 .redirect_behavior = .not_allowed,
1021 .server_header_buffer = http_headers_buffer,
1022 .extra_headers = &.{
1023 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
1024 .{ .name = "Git-Protocol", .value = "version=2" },
1025 },
1026 });
1020 try Packet.write(.{ .data = "done\n" }, &body);
1021 try Packet.write(.flush, &body);
1022
1023 fs.* = .{
1024 .request = try session.transport.request(.POST, upload_pack_uri, .{
1025 .redirect_behavior = .not_allowed,
1026 .extra_headers = &.{
1027 .{ .name = "Content-Type", .value = "application/x-git-upload-pack-request" },
1028 .{ .name = "Git-Protocol", .value = "version=2" },
1029 },
1030 }),
1031 .input = undefined,
1032 .reader = undefined,
1033 .remaining_len = undefined,
1034 };
1035 const request = &fs.request;
10271036 errdefer request.deinit();
1028 request.transfer_encoding = .{ .content_length = body.items.len };
1029 try request.send();
1030 try request.writeAll(body.items);
1031 try request.finish();
10321037
1033 try request.wait();
1034 if (request.response.status != .ok) return error.ProtocolError;
1038 try request.sendBodyComplete(body.buffered());
1039
1040 const response = try request.receiveHead(&.{});
1041 if (response.head.status != .ok) return error.ProtocolError;
10351042
1036 const reader = request.reader();
1043 const reader = response.reader(response_buffer);
10371044 // We are not interested in any of the sections of the returned fetch
10381045 // data other than the packfile section, since we aren't doing anything
10391046 // complex like ref negotiation (this is a fresh clone).
10401047 var state: enum { section_start, section_content } = .section_start;
10411048 while (true) {
1042 var buf: [Packet.max_data_length]u8 = undefined;
1043 const packet = try Packet.read(reader, &buf);
1049 const packet = try Packet.read(reader);
10441050 switch (state) {
10451051 .section_start => switch (packet) {
10461052 .data => |data| if (mem.eql(u8, Packet.normalizeText(data), "packfile")) {
1047 return .{ .request = request };
1053 fs.input = reader;
1054 fs.reader = .{
1055 .buffer = &.{},
1056 .vtable = &.{ .stream = FetchStream.stream },
1057 .seek = 0,
1058 .end = 0,
1059 };
1060 fs.remaining_len = 0;
1061 return;
10481062 } else {
10491063 state = .section_content;
10501064 },
......@@ -1061,20 +1075,23 @@ pub const Session = struct {
10611075
10621076 pub const FetchStream = struct {
10631077 request: std.http.Client.Request,
1064 buf: [Packet.max_data_length]u8 = undefined,
1065 pos: usize = 0,
1066 len: usize = 0,
1078 input: *std.Io.Reader,
1079 reader: std.Io.Reader,
1080 err: ?Error = null,
1081 remaining_len: usize,
10671082
1068 pub fn deinit(stream: *FetchStream) void {
1069 stream.request.deinit();
1083 pub fn deinit(fs: *FetchStream) void {
1084 fs.request.deinit();
10701085 }
10711086
1072 pub const ReadError = std.http.Client.Request.ReadError || error{
1087 pub const Error = error{
10731088 InvalidPacket,
10741089 ProtocolError,
10751090 UnexpectedPacket,
1091 WriteFailed,
1092 ReadFailed,
1093 EndOfStream,
10761094 };
1077 pub const Reader = std.io.GenericReader(*FetchStream, ReadError, read);
10781095
10791096 const StreamCode = enum(u8) {
10801097 pack_data = 1,
......@@ -1083,33 +1100,41 @@ pub const Session = struct {
10831100 _,
10841101 };
10851102
1086 pub fn reader(stream: *FetchStream) Reader {
1087 return .{ .context = stream };
1088 }
1089
1090 pub fn read(stream: *FetchStream, buf: []u8) !usize {
1091 if (stream.pos == stream.len) {
1103 pub fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
1104 const fs: *FetchStream = @alignCast(@fieldParentPtr("reader", r));
1105 const input = fs.input;
1106 if (fs.remaining_len == 0) {
10921107 while (true) {
1093 switch (try Packet.read(stream.request.reader(), &stream.buf)) {
1094 .flush => return 0,
1108 switch (Packet.read(input) catch |err| {
1109 fs.err = err;
1110 return error.ReadFailed;
1111 }) {
1112 .flush => return error.EndOfStream,
10951113 .data => |data| if (data.len > 1) switch (@as(StreamCode, @enumFromInt(data[0]))) {
10961114 .pack_data => {
1097 stream.pos = 1;
1098 stream.len = data.len;
1115 input.toss(1);
1116 fs.remaining_len = data.len;
10991117 break;
11001118 },
1101 .fatal_error => return error.ProtocolError,
1119 .fatal_error => {
1120 fs.err = error.ProtocolError;
1121 return error.ReadFailed;
1122 },
11021123 else => {},
11031124 },
1104 else => return error.UnexpectedPacket,
1125 else => {
1126 fs.err = error.UnexpectedPacket;
1127 return error.ReadFailed;
1128 },
11051129 }
11061130 }
11071131 }
1108
1109 const size = @min(buf.len, stream.len - stream.pos);
1110 @memcpy(buf[0..size], stream.buf[stream.pos .. stream.pos + size]);
1111 stream.pos += size;
1112 return size;
1132 const buf = limit.slice(try w.writableSliceGreedy(1));
1133 const n = @min(buf.len, fs.remaining_len);
1134 @memcpy(buf[0..n], input.buffered()[0..n]);
1135 input.toss(n);
1136 fs.remaining_len -= n;
1137 return n;
11131138 }
11141139 };
11151140};