authorgravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2024-04-09 22:22:08-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-04-10 02:11:54-07:00
logc4587dc9f46e15d4fb875a7675bc1aa22138c1ab
treefff3520b8a9ba7492b7ac1d35a5290c535c50ba5
parent215de3ee67f75e2405c177b262cb5c1cd8c8e343

Uri: propagate per-component encoding

This allows `std.Uri.resolve_inplace` to properly preserve the fact that `new` is already escaped but `base` may not be. I originally tried just moving `raw_uri` around, but it made uri resolution unmanagably complicated, so I instead added per-component information to `Uri` which allows extra allocations to be avoided when constructing uris with components from different sources, and in some cases, deferring the work all the way to when the uri is printed, where an allocator may not even be needed. Closes #19587

7 files changed, 520 insertions(+), 510 deletions(-)

lib/std/Uri.zig+309-334
......@@ -1,156 +1,157 @@
11//! Uniform Resource Identifier (URI) parsing roughly adhering to <https://tools.ietf.org/html/rfc3986>.
22//! Does not do perfect grammar and character class checking, but should be robust against URIs in the wild.
33
4const Uri = @This();
5const std = @import("std.zig");
6const testing = std.testing;
7const Allocator = std.mem.Allocator;
8
94scheme: []const u8,
10user: ?[]const u8 = null,
11password: ?[]const u8 = null,
12host: ?[]const u8 = null,
5user: ?Component = null,
6password: ?Component = null,
7host: ?Component = null,
138port: ?u16 = null,
14path: []const u8,
15query: ?[]const u8 = null,
16fragment: ?[]const u8 = null,
17
18/// Applies URI encoding and replaces all reserved characters with their respective %XX code.
19pub fn escapeString(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {
20 return escapeStringWithFn(allocator, input, isUnreserved);
21}
22
23pub fn escapePath(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {
24 return escapeStringWithFn(allocator, input, isPathChar);
25}
26
27pub fn escapeQuery(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {
28 return escapeStringWithFn(allocator, input, isQueryChar);
29}
30
31pub fn writeEscapedString(writer: anytype, input: []const u8) !void {
32 return writeEscapedStringWithFn(writer, input, isUnreserved);
33}
34
35pub fn writeEscapedPath(writer: anytype, input: []const u8) !void {
36 return writeEscapedStringWithFn(writer, input, isPathChar);
37}
38
39pub fn writeEscapedQuery(writer: anytype, input: []const u8) !void {
40 return writeEscapedStringWithFn(writer, input, isQueryChar);
41}
42
43pub fn escapeStringWithFn(allocator: Allocator, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) Allocator.Error![]u8 {
44 var outsize: usize = 0;
45 for (input) |c| {
46 outsize += if (keepUnescaped(c)) @as(usize, 1) else 3;
9path: Component = Component.empty,
10query: ?Component = null,
11fragment: ?Component = null,
12
13pub const Component = union(enum) {
14 /// Invalid characters in this component must be percent encoded
15 /// before being printed as part of a URI.
16 raw: []const u8,
17 /// This component is already percent-encoded, it can be printed
18 /// directly as part of a URI.
19 percent_encoded: []const u8,
20
21 pub const empty: Component = .{ .percent_encoded = "" };
22
23 pub fn isEmpty(component: Component) bool {
24 return switch (component) {
25 .raw, .percent_encoded => |string| string.len == 0,
26 };
4727 }
48 var output = try allocator.alloc(u8, outsize);
49 var outptr: usize = 0;
5028
51 for (input) |c| {
52 if (keepUnescaped(c)) {
53 output[outptr] = c;
54 outptr += 1;
55 } else {
56 var buf: [2]u8 = undefined;
57 _ = std.fmt.bufPrint(&buf, "{X:0>2}", .{c}) catch unreachable;
58
59 output[outptr + 0] = '%';
60 output[outptr + 1] = buf[0];
61 output[outptr + 2] = buf[1];
62 outptr += 3;
63 }
29 /// Allocates the result with `arena` only if needed, so the result should not be freed.
30 pub fn toRawMaybeAlloc(
31 component: Component,
32 arena: std.mem.Allocator,
33 ) std.mem.Allocator.Error![]const u8 {
34 return switch (component) {
35 .raw => |raw| raw,
36 .percent_encoded => |percent_encoded| if (std.mem.indexOfScalar(u8, percent_encoded, '%')) |_|
37 try std.fmt.allocPrint(arena, "{raw}", .{component})
38 else
39 percent_encoded,
40 };
6441 }
65 return output;
66}
6742
68pub fn writeEscapedStringWithFn(writer: anytype, input: []const u8, comptime keepUnescaped: fn (c: u8) bool) @TypeOf(writer).Error!void {
69 for (input) |c| {
70 if (keepUnescaped(c)) {
71 try writer.writeByte(c);
72 } else {
73 try writer.print("%{X:0>2}", .{c});
74 }
43 pub fn format(
44 component: Component,
45 comptime fmt_str: []const u8,
46 _: std.fmt.FormatOptions,
47 writer: anytype,
48 ) @TypeOf(writer).Error!void {
49 if (fmt_str.len == 0) {
50 try writer.print("std.Uri.Component{{ .{s} = \"{}\" }}", .{
51 @tagName(component),
52 std.zig.fmtEscapes(switch (component) {
53 .raw, .percent_encoded => |string| string,
54 }),
55 });
56 } else if (comptime std.mem.eql(u8, fmt_str, "raw")) switch (component) {
57 .raw => |raw| try writer.writeAll(raw),
58 .percent_encoded => |percent_encoded| {
59 var start: usize = 0;
60 var index: usize = 0;
61 while (std.mem.indexOfScalarPos(u8, percent_encoded, index, '%')) |percent| {
62 index = percent + 1;
63 if (percent_encoded.len - index < 2) continue;
64 const percent_encoded_char =
65 std.fmt.parseInt(u8, percent_encoded[index..][0..2], 16) catch continue;
66 try writer.print("{s}{c}", .{
67 percent_encoded[start..percent],
68 percent_encoded_char,
69 });
70 start = percent + 3;
71 index = percent + 3;
72 }
73 try writer.writeAll(percent_encoded[start..]);
74 },
75 } else if (comptime std.mem.eql(u8, fmt_str, "%")) switch (component) {
76 .raw => |raw| try percentEncode(writer, raw, isUnreserved),
77 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
78 } else if (comptime std.mem.eql(u8, fmt_str, "user")) switch (component) {
79 .raw => |raw| try percentEncode(writer, raw, isUserChar),
80 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
81 } else if (comptime std.mem.eql(u8, fmt_str, "password")) switch (component) {
82 .raw => |raw| try percentEncode(writer, raw, isPasswordChar),
83 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
84 } else if (comptime std.mem.eql(u8, fmt_str, "host")) switch (component) {
85 .raw => |raw| try percentEncode(writer, raw, isHostChar),
86 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
87 } else if (comptime std.mem.eql(u8, fmt_str, "path")) switch (component) {
88 .raw => |raw| try percentEncode(writer, raw, isPathChar),
89 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
90 } else if (comptime std.mem.eql(u8, fmt_str, "query")) switch (component) {
91 .raw => |raw| try percentEncode(writer, raw, isQueryChar),
92 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
93 } else if (comptime std.mem.eql(u8, fmt_str, "fragment")) switch (component) {
94 .raw => |raw| try percentEncode(writer, raw, isFragmentChar),
95 .percent_encoded => |percent_encoded| try writer.writeAll(percent_encoded),
96 } else @compileError("invalid format string '" ++ fmt_str ++ "'");
7597 }
76}
7798
78/// Parses a URI string and unescapes all %XX where XX is a valid hex number. Otherwise, verbatim copies
79/// them to the output.
80pub fn unescapeString(allocator: Allocator, input: []const u8) error{OutOfMemory}![]u8 {
81 var outsize: usize = 0;
82 var inptr: usize = 0;
83 while (inptr < input.len) {
84 if (input[inptr] == '%') {
85 inptr += 1;
86 if (inptr + 2 <= input.len) {
87 _ = std.fmt.parseInt(u8, input[inptr..][0..2], 16) catch {
88 outsize += 3;
89 inptr += 2;
90 continue;
91 };
92 inptr += 2;
93 outsize += 1;
94 } else {
95 outsize += 1;
96 }
97 } else {
98 inptr += 1;
99 outsize += 1;
99 pub fn percentEncode(
100 writer: anytype,
101 raw: []const u8,
102 comptime isValidChar: fn (u8) bool,
103 ) @TypeOf(writer).Error!void {
104 var start: usize = 0;
105 for (raw, 0..) |char, index| {
106 if (isValidChar(char)) continue;
107 try writer.print("{s}%{X:0>2}", .{ raw[start..index], char });
108 start = index + 1;
100109 }
110 try writer.writeAll(raw[start..]);
101111 }
112};
102113
103 var output = try allocator.alloc(u8, outsize);
104 var outptr: usize = 0;
105 inptr = 0;
106 while (inptr < input.len) {
107 if (input[inptr] == '%') {
108 inptr += 1;
109 if (inptr + 2 <= input.len) {
110 const value = std.fmt.parseInt(u8, input[inptr..][0..2], 16) catch {
111 output[outptr + 0] = input[inptr + 0];
112 output[outptr + 1] = input[inptr + 1];
113 inptr += 2;
114 outptr += 2;
114/// Percent decodes all %XX where XX is a valid hex number.
115/// `output` may alias `input` if `output.ptr <= input.ptr`.
116/// Mutates and returns a subslice of `output`.
117pub fn percentDecodeBackwards(output: []u8, input: []const u8) []u8 {
118 var input_index = input.len;
119 var output_index = output.len;
120 while (input_index > 0) {
121 if (input_index >= 3) {
122 const maybe_percent_encoded = input[input_index - 3 ..][0..3];
123 if (maybe_percent_encoded[0] == '%') {
124 if (std.fmt.parseInt(u8, maybe_percent_encoded[1..], 16)) |percent_encoded_char| {
125 input_index -= maybe_percent_encoded.len;
126 output_index -= 1;
127 output[output_index] = percent_encoded_char;
115128 continue;
116 };
117
118 output[outptr] = value;
119
120 inptr += 2;
121 outptr += 1;
122 } else {
123 output[outptr] = input[inptr - 1];
124 outptr += 1;
129 } else |_| {}
125130 }
126 } else {
127 output[outptr] = input[inptr];
128 inptr += 1;
129 outptr += 1;
130131 }
132 input_index -= 1;
133 output_index -= 1;
134 output[output_index] = input[input_index];
131135 }
132 return output;
136 return output[output_index..];
137}
138
139/// Percent decodes all %XX where XX is a valid hex number.
140/// Mutates and returns a subslice of `buffer`.
141pub fn percentDecodeInPlace(buffer: []u8) []u8 {
142 return percentDecodeBackwards(buffer, buffer);
133143}
134144
135145pub const ParseError = error{ UnexpectedCharacter, InvalidFormat, InvalidPort };
136146
137147/// Parses the URI or returns an error. This function is not compliant, but is required to parse
138148/// some forms of URIs in the wild, such as HTTP Location headers.
139/// The return value will contain unescaped strings pointing into the
140/// original `text`. Each component that is provided, will be non-`null`.
141pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
149/// The return value will contain strings pointing into the original `text`.
150/// Each component that is provided, will be non-`null`.
151pub fn parseAfterScheme(scheme: []const u8, text: []const u8) ParseError!Uri {
142152 var reader = SliceReader{ .slice = text };
143153
144 var uri = Uri{
145 .scheme = "",
146 .user = null,
147 .password = null,
148 .host = null,
149 .port = null,
150 .path = "", // path is always set, but empty by default.
151 .query = null,
152 .fragment = null,
153 };
154 var uri: Uri = .{ .scheme = scheme, .path = undefined };
154155
155156 if (reader.peekPrefix("//")) a: { // authority part
156157 std.debug.assert(reader.get().? == '/');
......@@ -167,12 +168,12 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
167168 const user_info = authority[0..index];
168169
169170 if (std.mem.indexOf(u8, user_info, ":")) |idx| {
170 uri.user = user_info[0..idx];
171 uri.user = .{ .percent_encoded = user_info[0..idx] };
171172 if (idx < user_info.len - 1) { // empty password is also "no password"
172 uri.password = user_info[idx + 1 ..];
173 uri.password = .{ .percent_encoded = user_info[idx + 1 ..] };
173174 }
174175 } else {
175 uri.user = user_info;
176 uri.user = .{ .percent_encoded = user_info };
176177 uri.password = null;
177178 }
178179 }
......@@ -205,19 +206,19 @@ pub fn parseWithoutScheme(text: []const u8) ParseError!Uri {
205206 }
206207
207208 if (start_of_host >= end_of_host) return error.InvalidFormat;
208 uri.host = authority[start_of_host..end_of_host];
209 uri.host = .{ .percent_encoded = authority[start_of_host..end_of_host] };
209210 }
210211
211 uri.path = reader.readUntil(isPathSeparator);
212 uri.path = .{ .percent_encoded = reader.readUntil(isPathSeparator) };
212213
213214 if ((reader.peek() orelse 0) == '?') { // query part
214215 std.debug.assert(reader.get().? == '?');
215 uri.query = reader.readUntil(isQuerySeparator);
216 uri.query = .{ .percent_encoded = reader.readUntil(isQuerySeparator) };
216217 }
217218
218219 if ((reader.peek() orelse 0) == '#') { // fragment part
219220 std.debug.assert(reader.get().? == '#');
220 uri.fragment = reader.readUntilEof();
221 uri.fragment = .{ .percent_encoded = reader.readUntilEof() };
221222 }
222223
223224 return uri;
......@@ -241,9 +242,6 @@ pub const WriteToStreamOptions = struct {
241242
242243 /// When true, include the fragment part of the URI. Ignored when `path` is false.
243244 fragment: bool = false,
244
245 /// When true, do not escape any part of the URI.
246 raw: bool = false,
247245};
248246
249247pub fn writeToStream(
......@@ -252,80 +250,51 @@ pub fn writeToStream(
252250 writer: anytype,
253251) @TypeOf(writer).Error!void {
254252 if (options.scheme) {
255 try writer.writeAll(uri.scheme);
256 try writer.writeAll(":");
257
253 try writer.print("{s}:", .{uri.scheme});
258254 if (options.authority and uri.host != null) {
259255 try writer.writeAll("//");
260256 }
261257 }
262
263258 if (options.authority) {
264259 if (options.authentication and uri.host != null) {
265260 if (uri.user) |user| {
266 try writer.writeAll(user);
261 try writer.print("{user}", .{user});
267262 if (uri.password) |password| {
268 try writer.writeAll(":");
269 try writer.writeAll(password);
263 try writer.print(":{password}", .{password});
270264 }
271 try writer.writeAll("@");
265 try writer.writeByte('@');
272266 }
273267 }
274
275268 if (uri.host) |host| {
276 try writer.writeAll(host);
277
278 if (uri.port) |port| {
279 try writer.writeAll(":");
280 try std.fmt.formatInt(port, 10, .lower, .{}, writer);
281 }
269 try writer.print("{host}", .{host});
270 if (uri.port) |port| try writer.print(":{d}", .{port});
282271 }
283272 }
284
285273 if (options.path) {
286 if (uri.path.len == 0) {
287 try writer.writeAll("/");
288 } else if (options.raw) {
289 try writer.writeAll(uri.path);
290 } else {
291 try writeEscapedPath(writer, uri.path);
274 try writer.print("{path}", .{
275 if (uri.path.isEmpty()) Uri.Component{ .percent_encoded = "/" } else uri.path,
276 });
277 if (options.query) {
278 if (uri.query) |query| try writer.print("?{query}", .{query});
279 }
280 if (options.fragment) {
281 if (uri.fragment) |fragment| try writer.print("#{fragment}", .{fragment});
292282 }
293
294 if (options.query) if (uri.query) |q| {
295 try writer.writeAll("?");
296 if (options.raw) {
297 try writer.writeAll(q);
298 } else {
299 try writeEscapedQuery(writer, q);
300 }
301 };
302
303 if (options.fragment) if (uri.fragment) |f| {
304 try writer.writeAll("#");
305 if (options.raw) {
306 try writer.writeAll(f);
307 } else {
308 try writeEscapedQuery(writer, f);
309 }
310 };
311283 }
312284}
313285
314286pub fn format(
315287 uri: Uri,
316 comptime fmt: []const u8,
317 options: std.fmt.FormatOptions,
288 comptime fmt_str: []const u8,
289 _: std.fmt.FormatOptions,
318290 writer: anytype,
319291) @TypeOf(writer).Error!void {
320 _ = options;
321
322 const scheme = comptime std.mem.indexOf(u8, fmt, ";") != null or fmt.len == 0;
323 const authentication = comptime std.mem.indexOf(u8, fmt, "@") != null or fmt.len == 0;
324 const authority = comptime std.mem.indexOf(u8, fmt, "+") != null or fmt.len == 0;
325 const path = comptime std.mem.indexOf(u8, fmt, "/") != null or fmt.len == 0;
326 const query = comptime std.mem.indexOf(u8, fmt, "?") != null or fmt.len == 0;
327 const fragment = comptime std.mem.indexOf(u8, fmt, "#") != null or fmt.len == 0;
328 const raw = comptime std.mem.indexOf(u8, fmt, "r") != null or fmt.len == 0;
292 const scheme = comptime std.mem.indexOfScalar(u8, fmt_str, ';') != null or fmt_str.len == 0;
293 const authentication = comptime std.mem.indexOfScalar(u8, fmt_str, '@') != null or fmt_str.len == 0;
294 const authority = comptime std.mem.indexOfScalar(u8, fmt_str, '+') != null or fmt_str.len == 0;
295 const path = comptime std.mem.indexOfScalar(u8, fmt_str, '/') != null or fmt_str.len == 0;
296 const query = comptime std.mem.indexOfScalar(u8, fmt_str, '?') != null or fmt_str.len == 0;
297 const fragment = comptime std.mem.indexOfScalar(u8, fmt_str, '#') != null or fmt_str.len == 0;
329298
330299 return writeToStream(uri, .{
331300 .scheme = scheme,
......@@ -334,12 +303,11 @@ pub fn format(
334303 .path = path,
335304 .query = query,
336305 .fragment = fragment,
337 .raw = raw,
338306 }, writer);
339307}
340308
341309/// Parses the URI or returns an error.
342/// The return value will contain unescaped strings pointing into the
310/// The return value will contain strings pointing into the
343311/// original `text`. Each component that is provided, will be non-`null`.
344312pub fn parse(text: []const u8) ParseError!Uri {
345313 var reader: SliceReader = .{ .slice = text };
......@@ -353,42 +321,32 @@ pub fn parse(text: []const u8) ParseError!Uri {
353321 return error.InvalidFormat;
354322 }
355323
356 var uri = try parseWithoutScheme(reader.readUntilEof());
357 uri.scheme = scheme;
358
359 return uri;
324 return parseAfterScheme(scheme, reader.readUntilEof());
360325}
361326
362pub const ResolveInplaceError = ParseError || error{OutOfMemory};
327pub const ResolveInPlaceError = ParseError || error{NoSpaceLeft};
363328
364329/// Resolves a URI against a base URI, conforming to RFC 3986, Section 5.
365/// Copies `new` to the beginning of `aux_buf`, allowing the slices to overlap,
330/// Copies `new` to the beginning of `aux_buf.*`, allowing the slices to overlap,
366331/// then parses `new` as a URI, and then resolves the path in place.
367332/// If a merge needs to take place, the newly constructed path will be stored
368/// in `aux_buf` just after the copied `new`.
369pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplaceError!Uri {
370 std.mem.copyForwards(u8, aux_buf, new);
333/// in `aux_buf.*` just after the copied `new`, and `aux_buf.*` will be modified
334/// to only contain the remaining unused space.
335pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: *[]u8) ResolveInPlaceError!Uri {
336 std.mem.copyForwards(u8, aux_buf.*, new);
371337 // At this point, new is an invalid pointer.
372 const new_mut = aux_buf[0..new.len];
373
374 const new_parsed, const has_scheme = p: {
375 break :p .{
376 parse(new_mut) catch |first_err| {
377 break :p .{
378 parseWithoutScheme(new_mut) catch return first_err,
379 false,
380 };
381 },
382 true,
383 };
384 };
338 const new_mut = aux_buf.*[0..new.len];
339 aux_buf.* = aux_buf.*[new.len..];
385340
341 const new_parsed = parse(new_mut) catch |err|
342 (parseAfterScheme("", new_mut) catch return err);
386343 // As you can see above, `new_mut` is not a const pointer.
387 const new_path: []u8 = @constCast(new_parsed.path);
344 const new_path: []u8 = @constCast(new_parsed.path.percent_encoded);
388345
389 if (has_scheme) return .{
346 if (new_parsed.scheme.len > 0) return .{
390347 .scheme = new_parsed.scheme,
391348 .user = new_parsed.user,
349 .password = new_parsed.password,
392350 .host = new_parsed.host,
393351 .port = new_parsed.port,
394352 .path = remove_dot_segments(new_path),
......@@ -399,6 +357,7 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace
399357 if (new_parsed.host) |host| return .{
400358 .scheme = base.scheme,
401359 .user = new_parsed.user,
360 .password = new_parsed.password,
402361 .host = host,
403362 .port = new_parsed.port,
404363 .path = remove_dot_segments(new_path),
......@@ -406,28 +365,21 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace
406365 .fragment = new_parsed.fragment,
407366 };
408367
409 const path, const query = b: {
410 if (new_path.len == 0)
411 break :b .{
412 base.path,
413 new_parsed.query orelse base.query,
414 };
415
416 if (new_path[0] == '/')
417 break :b .{
418 remove_dot_segments(new_path),
419 new_parsed.query,
420 };
421
422 break :b .{
423 try merge_paths(base.path, new_path, aux_buf[new_mut.len..]),
424 new_parsed.query,
425 };
368 const path, const query = if (new_path.len == 0) .{
369 base.path,
370 new_parsed.query orelse base.query,
371 } else if (new_path[0] == '/') .{
372 remove_dot_segments(new_path),
373 new_parsed.query,
374 } else .{
375 try merge_paths(base.path, new_path, aux_buf),
376 new_parsed.query,
426377 };
427378
428379 return .{
429380 .scheme = base.scheme,
430381 .user = base.user,
382 .password = base.password,
431383 .host = base.host,
432384 .port = base.port,
433385 .path = path,
......@@ -437,7 +389,7 @@ pub fn resolve_inplace(base: Uri, new: []const u8, aux_buf: []u8) ResolveInplace
437389}
438390
439391/// In-place implementation of RFC 3986, Section 5.2.4.
440fn remove_dot_segments(path: []u8) []u8 {
392fn remove_dot_segments(path: []u8) Component {
441393 var in_i: usize = 0;
442394 var out_i: usize = 0;
443395 while (in_i < path.len) {
......@@ -476,28 +428,28 @@ fn remove_dot_segments(path: []u8) []u8 {
476428 }
477429 }
478430 }
479 return path[0..out_i];
431 return .{ .percent_encoded = path[0..out_i] };
480432}
481433
482434test remove_dot_segments {
483435 {
484436 var buffer = "/a/b/c/./../../g".*;
485 try std.testing.expectEqualStrings("/a/g", remove_dot_segments(&buffer));
437 try std.testing.expectEqualStrings("/a/g", remove_dot_segments(&buffer).percent_encoded);
486438 }
487439}
488440
489441/// 5.2.3. Merge Paths
490fn merge_paths(base: []const u8, new: []u8, aux: []u8) error{OutOfMemory}![]u8 {
491 if (aux.len < base.len + 1 + new.len) return error.OutOfMemory;
492 if (base.len == 0) {
493 aux[0] = '/';
494 @memcpy(aux[1..][0..new.len], new);
495 return remove_dot_segments(aux[0 .. new.len + 1]);
442fn merge_paths(base: Component, new: []u8, aux_buf: *[]u8) error{NoSpaceLeft}!Component {
443 var aux = std.io.fixedBufferStream(aux_buf.*);
444 if (!base.isEmpty()) {
445 try aux.writer().print("{path}", .{base});
446 aux.pos = std.mem.lastIndexOfScalar(u8, aux.getWritten(), '/') orelse
447 return remove_dot_segments(new);
496448 }
497 const pos = std.mem.lastIndexOfScalar(u8, base, '/') orelse return remove_dot_segments(new);
498 @memcpy(aux[0 .. pos + 1], base[0 .. pos + 1]);
499 @memcpy(aux[pos + 1 ..][0..new.len], new);
500 return remove_dot_segments(aux[0 .. pos + 1 + new.len]);
449 try aux.writer().print("/{s}", .{new});
450 const merged_path = remove_dot_segments(aux.getWritten());
451 aux_buf.* = aux_buf.*[merged_path.percent_encoded.len..];
452 return merged_path;
501453}
502454
503455const SliceReader = struct {
......@@ -561,13 +513,6 @@ fn isSchemeChar(c: u8) bool {
561513 };
562514}
563515
564fn isAuthoritySeparator(c: u8) bool {
565 return switch (c) {
566 '/', '?', '#' => true,
567 else => false,
568 };
569}
570
571516/// reserved = gen-delims / sub-delims
572517fn isReserved(c: u8) bool {
573518 return isGenLimit(c) or isSubLimit(c);
......@@ -598,19 +543,40 @@ fn isUnreserved(c: u8) bool {
598543 };
599544}
600545
601fn isPathSeparator(c: u8) bool {
602 return switch (c) {
603 '?', '#' => true,
604 else => false,
605 };
546fn isUserChar(c: u8) bool {
547 return isUnreserved(c) or isSubLimit(c);
548}
549
550fn isPasswordChar(c: u8) bool {
551 return isUserChar(c) or c == ':';
552}
553
554fn isHostChar(c: u8) bool {
555 return isPasswordChar(c) or c == '[' or c == ']';
606556}
607557
608558fn isPathChar(c: u8) bool {
609 return isUnreserved(c) or isSubLimit(c) or c == '/' or c == ':' or c == '@';
559 return isUserChar(c) or c == '/' or c == ':' or c == '@';
610560}
611561
612562fn isQueryChar(c: u8) bool {
613 return isPathChar(c) or c == '?' or c == '%';
563 return isPathChar(c) or c == '?';
564}
565
566const isFragmentChar = isQueryChar;
567
568fn isAuthoritySeparator(c: u8) bool {
569 return switch (c) {
570 '/', '?', '#' => true,
571 else => false,
572 };
573}
574
575fn isPathSeparator(c: u8) bool {
576 return switch (c) {
577 '?', '#' => true,
578 else => false,
579 };
614580}
615581
616582fn isQuerySeparator(c: u8) bool {
......@@ -623,92 +589,92 @@ fn isQuerySeparator(c: u8) bool {
623589test "basic" {
624590 const parsed = try parse("https://ziglang.org/download");
625591 try testing.expectEqualStrings("https", parsed.scheme);
626 try testing.expectEqualStrings("ziglang.org", parsed.host orelse return error.UnexpectedNull);
627 try testing.expectEqualStrings("/download", parsed.path);
592 try testing.expectEqualStrings("ziglang.org", parsed.host.?.percent_encoded);
593 try testing.expectEqualStrings("/download", parsed.path.percent_encoded);
628594 try testing.expectEqual(@as(?u16, null), parsed.port);
629595}
630596
631597test "with port" {
632598 const parsed = try parse("http://example:1337/");
633599 try testing.expectEqualStrings("http", parsed.scheme);
634 try testing.expectEqualStrings("example", parsed.host orelse return error.UnexpectedNull);
635 try testing.expectEqualStrings("/", parsed.path);
600 try testing.expectEqualStrings("example", parsed.host.?.percent_encoded);
601 try testing.expectEqualStrings("/", parsed.path.percent_encoded);
636602 try testing.expectEqual(@as(?u16, 1337), parsed.port);
637603}
638604
639605test "should fail gracefully" {
640 try std.testing.expectEqual(@as(ParseError!Uri, error.InvalidFormat), parse("foobar://"));
606 try std.testing.expectError(error.InvalidFormat, parse("foobar://"));
641607}
642608
643609test "file" {
644610 const parsed = try parse("file:///");
645 try std.testing.expectEqualSlices(u8, "file", parsed.scheme);
646 try std.testing.expectEqual(@as(?[]const u8, null), parsed.host);
647 try std.testing.expectEqualSlices(u8, "/", parsed.path);
611 try std.testing.expectEqualStrings("file", parsed.scheme);
612 try std.testing.expectEqual(@as(?Component, null), parsed.host);
613 try std.testing.expectEqualStrings("/", parsed.path.percent_encoded);
648614
649615 const parsed2 = try parse("file:///an/absolute/path/to/something");
650 try std.testing.expectEqualSlices(u8, "file", parsed2.scheme);
651 try std.testing.expectEqual(@as(?[]const u8, null), parsed2.host);
652 try std.testing.expectEqualSlices(u8, "/an/absolute/path/to/something", parsed2.path);
616 try std.testing.expectEqualStrings("file", parsed2.scheme);
617 try std.testing.expectEqual(@as(?Component, null), parsed2.host);
618 try std.testing.expectEqualStrings("/an/absolute/path/to/something", parsed2.path.percent_encoded);
653619
654620 const parsed3 = try parse("file://localhost/an/absolute/path/to/another/thing/");
655 try std.testing.expectEqualSlices(u8, "file", parsed3.scheme);
656 try std.testing.expectEqualSlices(u8, "localhost", parsed3.host.?);
657 try std.testing.expectEqualSlices(u8, "/an/absolute/path/to/another/thing/", parsed3.path);
621 try std.testing.expectEqualStrings("file", parsed3.scheme);
622 try std.testing.expectEqualStrings("localhost", parsed3.host.?.percent_encoded);
623 try std.testing.expectEqualStrings("/an/absolute/path/to/another/thing/", parsed3.path.percent_encoded);
658624}
659625
660626test "scheme" {
661 try std.testing.expectEqualSlices(u8, "http", (try parse("http:_")).scheme);
662 try std.testing.expectEqualSlices(u8, "scheme-mee", (try parse("scheme-mee:_")).scheme);
663 try std.testing.expectEqualSlices(u8, "a.b.c", (try parse("a.b.c:_")).scheme);
664 try std.testing.expectEqualSlices(u8, "ab+", (try parse("ab+:_")).scheme);
665 try std.testing.expectEqualSlices(u8, "X+++", (try parse("X+++:_")).scheme);
666 try std.testing.expectEqualSlices(u8, "Y+-.", (try parse("Y+-.:_")).scheme);
627 try std.testing.expectEqualStrings("http", (try parse("http:_")).scheme);
628 try std.testing.expectEqualStrings("scheme-mee", (try parse("scheme-mee:_")).scheme);
629 try std.testing.expectEqualStrings("a.b.c", (try parse("a.b.c:_")).scheme);
630 try std.testing.expectEqualStrings("ab+", (try parse("ab+:_")).scheme);
631 try std.testing.expectEqualStrings("X+++", (try parse("X+++:_")).scheme);
632 try std.testing.expectEqualStrings("Y+-.", (try parse("Y+-.:_")).scheme);
667633}
668634
669635test "authority" {
670 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://hostname")).host.?);
636 try std.testing.expectEqualStrings("hostname", (try parse("scheme://hostname")).host.?.percent_encoded);
671637
672 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://userinfo@hostname")).host.?);
673 try std.testing.expectEqualSlices(u8, "userinfo", (try parse("scheme://userinfo@hostname")).user.?);
674 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@hostname")).password);
675 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@")).host);
638 try std.testing.expectEqualStrings("hostname", (try parse("scheme://userinfo@hostname")).host.?.percent_encoded);
639 try std.testing.expectEqualStrings("userinfo", (try parse("scheme://userinfo@hostname")).user.?.percent_encoded);
640 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://userinfo@hostname")).password);
641 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://userinfo@")).host);
676642
677 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://user:password@hostname")).host.?);
678 try std.testing.expectEqualSlices(u8, "user", (try parse("scheme://user:password@hostname")).user.?);
679 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://user:password@hostname")).password.?);
643 try std.testing.expectEqualStrings("hostname", (try parse("scheme://user:password@hostname")).host.?.percent_encoded);
644 try std.testing.expectEqualStrings("user", (try parse("scheme://user:password@hostname")).user.?.percent_encoded);
645 try std.testing.expectEqualStrings("password", (try parse("scheme://user:password@hostname")).password.?.percent_encoded);
680646
681 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://hostname:0")).host.?);
647 try std.testing.expectEqualStrings("hostname", (try parse("scheme://hostname:0")).host.?.percent_encoded);
682648 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://hostname:1234")).port.?);
683649
684 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://userinfo@hostname:1234")).host.?);
650 try std.testing.expectEqualStrings("hostname", (try parse("scheme://userinfo@hostname:1234")).host.?.percent_encoded);
685651 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://userinfo@hostname:1234")).port.?);
686 try std.testing.expectEqualSlices(u8, "userinfo", (try parse("scheme://userinfo@hostname:1234")).user.?);
687 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://userinfo@hostname:1234")).password);
652 try std.testing.expectEqualStrings("userinfo", (try parse("scheme://userinfo@hostname:1234")).user.?.percent_encoded);
653 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://userinfo@hostname:1234")).password);
688654
689 try std.testing.expectEqualSlices(u8, "hostname", (try parse("scheme://user:password@hostname:1234")).host.?);
655 try std.testing.expectEqualStrings("hostname", (try parse("scheme://user:password@hostname:1234")).host.?.percent_encoded);
690656 try std.testing.expectEqual(@as(u16, 1234), (try parse("scheme://user:password@hostname:1234")).port.?);
691 try std.testing.expectEqualSlices(u8, "user", (try parse("scheme://user:password@hostname:1234")).user.?);
692 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://user:password@hostname:1234")).password.?);
657 try std.testing.expectEqualStrings("user", (try parse("scheme://user:password@hostname:1234")).user.?.percent_encoded);
658 try std.testing.expectEqualStrings("password", (try parse("scheme://user:password@hostname:1234")).password.?.percent_encoded);
693659}
694660
695661test "authority.password" {
696 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username@a")).user.?);
697 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://username@a")).password);
662 try std.testing.expectEqualStrings("username", (try parse("scheme://username@a")).user.?.percent_encoded);
663 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://username@a")).password);
698664
699 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username:@a")).user.?);
700 try std.testing.expectEqual(@as(?[]const u8, null), (try parse("scheme://username:@a")).password);
665 try std.testing.expectEqualStrings("username", (try parse("scheme://username:@a")).user.?.percent_encoded);
666 try std.testing.expectEqual(@as(?Component, null), (try parse("scheme://username:@a")).password);
701667
702 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username:password@a")).user.?);
703 try std.testing.expectEqualSlices(u8, "password", (try parse("scheme://username:password@a")).password.?);
668 try std.testing.expectEqualStrings("username", (try parse("scheme://username:password@a")).user.?.percent_encoded);
669 try std.testing.expectEqualStrings("password", (try parse("scheme://username:password@a")).password.?.percent_encoded);
704670
705 try std.testing.expectEqualSlices(u8, "username", (try parse("scheme://username::@a")).user.?);
706 try std.testing.expectEqualSlices(u8, ":", (try parse("scheme://username::@a")).password.?);
671 try std.testing.expectEqualStrings("username", (try parse("scheme://username::@a")).user.?.percent_encoded);
672 try std.testing.expectEqualStrings(":", (try parse("scheme://username::@a")).password.?.percent_encoded);
707673}
708674
709675fn testAuthorityHost(comptime hostlist: anytype) !void {
710676 inline for (hostlist) |hostname| {
711 try std.testing.expectEqualSlices(u8, hostname, (try parse("scheme://" ++ hostname)).host.?);
677 try std.testing.expectEqualStrings(hostname, (try parse("scheme://" ++ hostname)).host.?.percent_encoded);
712678 }
713679}
714680
......@@ -761,11 +727,11 @@ test "RFC example 1" {
761727 .scheme = uri[0..3],
762728 .user = null,
763729 .password = null,
764 .host = uri[6..17],
730 .host = .{ .percent_encoded = uri[6..17] },
765731 .port = 8042,
766 .path = uri[22..33],
767 .query = uri[34..45],
768 .fragment = uri[46..50],
732 .path = .{ .percent_encoded = uri[22..33] },
733 .query = .{ .percent_encoded = uri[34..45] },
734 .fragment = .{ .percent_encoded = uri[46..50] },
769735 }, try parse(uri));
770736}
771737
......@@ -777,7 +743,7 @@ test "RFC example 2" {
777743 .password = null,
778744 .host = null,
779745 .port = null,
780 .path = uri[4..],
746 .path = .{ .percent_encoded = uri[4..] },
781747 .query = null,
782748 .fragment = null,
783749 }, try parse(uri));
......@@ -838,55 +804,60 @@ test "Special test" {
838804 _ = try parse("https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be&t=0");
839805}
840806
841test "URI escaping" {
842 const input = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
843 const expected = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";
807test "URI percent encoding" {
808 try std.testing.expectFmt(
809 "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad",
810 "{%}",
811 .{Component{ .raw = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad" }},
812 );
813}
844814
845 const actual = try escapeString(std.testing.allocator, input);
846 defer std.testing.allocator.free(actual);
815test "URI percent decoding" {
816 {
817 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
818 var input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad".*;
847819
848 try std.testing.expectEqualSlices(u8, expected, actual);
849}
820 try std.testing.expectFmt(expected, "{raw}", .{Component{ .percent_encoded = &input }});
821
822 var output: [expected.len]u8 = undefined;
823 try std.testing.expectEqualStrings(percentDecodeBackwards(&output, &input), expected);
824
825 try std.testing.expectEqualStrings(expected, percentDecodeInPlace(&input));
826 }
850827
851test "URI unescaping" {
852 const input = "%5C%C3%B6%2F%20%C3%A4%C3%B6%C3%9F%20~~.adas-https%3A%2F%2Fcanvas%3A123%2F%23ads%26%26sad";
853 const expected = "\\ö/ äöß ~~.adas-https://canvas:123/#ads&&sad";
828 {
829 const expected = "/abc%";
830 var input = expected.*;
854831
855 const actual = try unescapeString(std.testing.allocator, input);
856 defer std.testing.allocator.free(actual);
832 try std.testing.expectFmt(expected, "{raw}", .{Component{ .percent_encoded = &input }});
857833
858 try std.testing.expectEqualSlices(u8, expected, actual);
834 var output: [expected.len]u8 = undefined;
835 try std.testing.expectEqualStrings(percentDecodeBackwards(&output, &input), expected);
859836
860 const decoded = try unescapeString(std.testing.allocator, "/abc%");
861 defer std.testing.allocator.free(decoded);
862 try std.testing.expectEqualStrings("/abc%", decoded);
837 try std.testing.expectEqualStrings(expected, percentDecodeInPlace(&input));
838 }
863839}
864840
865test "URI query escaping" {
841test "URI query encoding" {
866842 const address = "https://objects.githubusercontent.com/?response-content-type=application%2Foctet-stream";
867843 const parsed = try Uri.parse(address);
868844
869 // format the URI to escape it
870 const formatted_uri = try std.fmt.allocPrint(std.testing.allocator, "{/?}", .{parsed});
871 defer std.testing.allocator.free(formatted_uri);
872 try std.testing.expectEqualStrings("/?response-content-type=application%2Foctet-stream", formatted_uri);
845 // format the URI to percent encode it
846 try std.testing.expectFmt("/?response-content-type=application%2Foctet-stream", "{/?}", .{parsed});
873847}
874848
875849test "format" {
876 const uri = Uri{
850 const uri: Uri = .{
877851 .scheme = "file",
878852 .user = null,
879853 .password = null,
880854 .host = null,
881855 .port = null,
882 .path = "/foo/bar/baz",
856 .path = .{ .raw = "/foo/bar/baz" },
883857 .query = null,
884858 .fragment = null,
885859 };
886 var buf = std.ArrayList(u8).init(std.testing.allocator);
887 defer buf.deinit();
888 try buf.writer().print("{;/?#}", .{uri});
889 try std.testing.expectEqualSlices(u8, "file:/foo/bar/baz", buf.items);
860 try std.testing.expectFmt("file:/foo/bar/baz", "{;/?#}", .{uri});
890861}
891862
892863test "URI malformed input" {
......@@ -894,3 +865,7 @@ test "URI malformed input" {
894865 try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://]@["));
895866 try std.testing.expectError(error.InvalidFormat, std.Uri.parse("http://lo]s\x85hc@[/8\x10?0Q"));
896867}
868
869const std = @import("std.zig");
870const testing = std.testing;
871const Uri = @This();
lib/std/http/Client.zig+108-128
......@@ -771,17 +771,41 @@ pub const Request = struct {
771771 req.client.connection_pool.release(req.client.allocator, req.connection.?);
772772 req.connection = null;
773773
774 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
774 var server_header = std.heap.FixedBufferAllocator.init(req.response.parser.header_bytes_buffer);
775 defer req.response.parser.header_bytes_buffer = server_header.buffer[server_header.end_index..];
776 const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
777
778 const new_host = valid_uri.host.?.raw;
779 const prev_host = req.uri.host.?.raw;
780 const keep_privileged_headers =
781 std.ascii.eqlIgnoreCase(valid_uri.scheme, req.uri.scheme) and
782 std.ascii.endsWithIgnoreCase(new_host, prev_host) and
783 (new_host.len == prev_host.len or new_host[new_host.len - prev_host.len - 1] == '.');
784 if (!keep_privileged_headers) {
785 // When redirecting to a different domain, strip privileged headers.
786 req.privileged_headers = &.{};
787 }
775788
776 const port: u16 = uri.port orelse switch (protocol) {
777 .plain => 80,
778 .tls => 443,
779 };
789 if (switch (req.response.status) {
790 .see_other => true,
791 .moved_permanently, .found => req.method == .POST,
792 else => false,
793 }) {
794 // A redirect to a GET must change the method and remove the body.
795 req.method = .GET;
796 req.transfer_encoding = .none;
797 req.headers.content_type = .omit;
798 }
780799
781 const host = uri.host orelse return error.UriMissingHost;
800 if (req.transfer_encoding != .none) {
801 // The request body has already been sent. The request is
802 // still in a valid state, but the redirect must be handled
803 // manually.
804 return error.RedirectRequiresResend;
805 }
782806
783 req.uri = uri;
784 req.connection = try req.client.connect(host, port, protocol);
807 req.uri = valid_uri;
808 req.connection = try req.client.connect(new_host, valid_uri.port.?, protocol);
785809 req.redirect_behavior.subtractOne();
786810 req.response.parser.reset();
787811
......@@ -796,13 +820,8 @@ pub const Request = struct {
796820
797821 pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding };
798822
799 pub const SendOptions = struct {
800 /// Specifies that the uri is already escaped.
801 raw_uri: bool = false,
802 };
803
804823 /// Send the HTTP request headers to the server.
805 pub fn send(req: *Request, options: SendOptions) SendError!void {
824 pub fn send(req: *Request) SendError!void {
806825 if (!req.method.requestHasBody() and req.transfer_encoding != .none)
807826 return error.UnsupportedTransferEncoding;
808827
......@@ -821,7 +840,6 @@ pub const Request = struct {
821840 .authority = connection.proxied,
822841 .path = true,
823842 .query = true,
824 .raw = options.raw_uri,
825843 }, w);
826844 }
827845 try w.writeByte(' ');
......@@ -1038,55 +1056,19 @@ pub const Request = struct {
10381056 const location = req.response.location orelse
10391057 return error.HttpRedirectLocationMissing;
10401058
1041 // This mutates the beginning of header_buffer and uses that
1042 // for the backing memory of the returned new_uri.
1043 const header_buffer = req.response.parser.header_bytes_buffer;
1044 const new_uri = req.uri.resolve_inplace(location, header_buffer) catch
1045 return error.HttpRedirectLocationInvalid;
1046
1047 // The new URI references the beginning of header_bytes_buffer memory.
1048 // That memory will be kept, but everything after it will be
1049 // reused by the subsequent request. In other words,
1050 // header_bytes_buffer must be large enough to store all
1051 // redirect locations as well as the final request header.
1052 const path_end = new_uri.path.ptr + new_uri.path.len;
1053 // https://github.com/ziglang/zig/issues/1738
1054 const path_offset = @intFromPtr(path_end) - @intFromPtr(header_buffer.ptr);
1055 const end_offset = @max(path_offset, location.len);
1056 req.response.parser.header_bytes_buffer = header_buffer[end_offset..];
1057
1058 const is_same_domain_or_subdomain =
1059 std.ascii.endsWithIgnoreCase(new_uri.host.?, req.uri.host.?) and
1060 (new_uri.host.?.len == req.uri.host.?.len or
1061 new_uri.host.?[new_uri.host.?.len - req.uri.host.?.len - 1] == '.');
1062
1063 if (new_uri.host == null or !is_same_domain_or_subdomain or
1064 !std.ascii.eqlIgnoreCase(new_uri.scheme, req.uri.scheme))
1065 {
1066 // When redirecting to a different domain, strip privileged headers.
1067 req.privileged_headers = &.{};
1068 }
1069
1070 if (switch (req.response.status) {
1071 .see_other => true,
1072 .moved_permanently, .found => req.method == .POST,
1073 else => false,
1074 }) {
1075 // A redirect to a GET must change the method and remove the body.
1076 req.method = .GET;
1077 req.transfer_encoding = .none;
1078 req.headers.content_type = .omit;
1079 }
1080
1081 if (req.transfer_encoding != .none) {
1082 // The request body has already been sent. The request is
1083 // still in a valid state, but the redirect must be handled
1084 // manually.
1085 return error.RedirectRequiresResend;
1086 }
1087
1088 try req.redirect(new_uri);
1089 try req.send(.{});
1059 // This mutates the beginning of header_bytes_buffer and uses that
1060 // for the backing memory of the returned Uri.
1061 try req.redirect(req.uri.resolve_inplace(
1062 location,
1063 &req.response.parser.header_bytes_buffer,
1064 ) catch |err| switch (err) {
1065 error.UnexpectedCharacter,
1066 error.InvalidFormat,
1067 error.InvalidPort,
1068 => return error.HttpRedirectLocationInvalid,
1069 error.NoSpaceLeft => return error.HttpHeadersOversize,
1070 });
1071 try req.send();
10901072 } else {
10911073 req.response.skip = false;
10921074 if (!req.response.parser.done) {
......@@ -1264,30 +1246,25 @@ fn createProxyFromEnvVar(arena: Allocator, env_var_names: []const []const u8) !?
12641246 };
12651247 } else return null;
12661248
1267 const uri = Uri.parse(content) catch try Uri.parseWithoutScheme(content);
1268
1269 const protocol = if (uri.scheme.len == 0)
1270 .plain // No scheme, assume http://
1271 else
1272 protocol_map.get(uri.scheme) orelse return null; // Unknown scheme, ignore
1273
1274 const host = uri.host orelse return error.HttpProxyMissingHost;
1249 const uri = Uri.parse(content) catch try Uri.parseAfterScheme("http", content);
1250 const protocol, const valid_uri = validateUri(uri, arena) catch |err| switch (err) {
1251 error.UnsupportedUriScheme => return null,
1252 error.UriMissingHost => return error.HttpProxyMissingHost,
1253 error.OutOfMemory => |e| return e,
1254 };
12751255
1276 const authorization: ?[]const u8 = if (uri.user != null or uri.password != null) a: {
1277 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(uri));
1278 assert(basic_authorization.value(uri, authorization).len == authorization.len);
1256 const authorization: ?[]const u8 = if (valid_uri.user != null or valid_uri.password != null) a: {
1257 const authorization = try arena.alloc(u8, basic_authorization.valueLengthFromUri(valid_uri));
1258 assert(basic_authorization.value(valid_uri, authorization).len == authorization.len);
12791259 break :a authorization;
12801260 } else null;
12811261
12821262 const proxy = try arena.create(Proxy);
12831263 proxy.* = .{
12841264 .protocol = protocol,
1285 .host = host,
1265 .host = valid_uri.host.?.raw,
12861266 .authorization = authorization,
1287 .port = uri.port orelse switch (protocol) {
1288 .plain => 80,
1289 .tls => 443,
1290 },
1267 .port = valid_uri.port.?,
12911268 .supports_connect = true,
12921269 };
12931270 return proxy;
......@@ -1305,24 +1282,26 @@ pub const basic_authorization = struct {
13051282 }
13061283
13071284 pub fn valueLengthFromUri(uri: Uri) usize {
1308 return valueLength(
1309 if (uri.user) |user| user.len else 0,
1310 if (uri.password) |password| password.len else 0,
1311 );
1285 var stream = std.io.countingWriter(std.io.null_writer);
1286 try stream.writer().print("{user}", .{uri.user orelse Uri.Component.empty});
1287 const user_len = stream.bytes_written;
1288 stream.bytes_written = 0;
1289 try stream.writer().print("{password}", .{uri.password orelse Uri.Component.empty});
1290 const password_len = stream.bytes_written;
1291 return valueLength(@intCast(user_len), @intCast(password_len));
13121292 }
13131293
13141294 pub fn value(uri: Uri, out: []u8) []u8 {
1315 assert(uri.user == null or uri.user.?.len <= max_user_len);
1316 assert(uri.password == null or uri.password.?.len <= max_password_len);
1317
1318 @memcpy(out[0..prefix.len], prefix);
1319
13201295 var buf: [max_user_len + ":".len + max_password_len]u8 = undefined;
1321 const unencoded = std.fmt.bufPrint(&buf, "{s}:{s}", .{
1322 uri.user orelse "", uri.password orelse "",
1323 }) catch unreachable;
1324 const base64 = std.base64.standard.Encoder.encode(out[prefix.len..], unencoded);
1296 var stream = std.io.fixedBufferStream(&buf);
1297 stream.writer().print("{user}", .{uri.user orelse Uri.Component.empty}) catch
1298 unreachable;
1299 assert(stream.pos <= max_user_len);
1300 stream.writer().print(":{password}", .{uri.password orelse Uri.Component.empty}) catch
1301 unreachable;
13251302
1303 @memcpy(out[0..prefix.len], prefix);
1304 const base64 = std.base64.standard.Encoder.encode(out[prefix.len..], stream.getWritten());
13261305 return out[0 .. prefix.len + base64.len];
13271306 }
13281307};
......@@ -1337,8 +1316,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec
13371316 .host = host,
13381317 .port = port,
13391318 .protocol = protocol,
1340 })) |node|
1341 return node;
1319 })) |node| return node;
13421320
13431321 if (disable_tls and protocol == .tls)
13441322 return error.TlsInitializationFailed;
......@@ -1449,19 +1427,12 @@ pub fn connectTunnel(
14491427 client.connection_pool.release(client.allocator, conn);
14501428 }
14511429
1452 const uri: Uri = .{
1430 var buffer: [8096]u8 = undefined;
1431 var req = client.open(.CONNECT, .{
14531432 .scheme = "http",
1454 .user = null,
1455 .password = null,
1456 .host = tunnel_host,
1433 .host = .{ .raw = tunnel_host },
14571434 .port = tunnel_port,
1458 .path = "",
1459 .query = null,
1460 .fragment = null,
1461 };
1462
1463 var buffer: [8096]u8 = undefined;
1464 var req = client.open(.CONNECT, uri, .{
1435 }, .{
14651436 .redirect_behavior = .unhandled,
14661437 .connection = conn,
14671438 .server_header_buffer = &buffer,
......@@ -1471,7 +1442,7 @@ pub fn connectTunnel(
14711442 };
14721443 defer req.deinit();
14731444
1474 req.send(.{ .raw_uri = true }) catch |err| break :tunnel err;
1445 req.send() catch |err| break :tunnel err;
14751446 req.wait() catch |err| break :tunnel err;
14761447
14771448 if (req.response.status.class() == .server_error) {
......@@ -1500,7 +1471,7 @@ pub fn connectTunnel(
15001471}
15011472
15021473// Prevents a dependency loop in open()
1503const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused };
1474const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUriScheme, ConnectionRefused };
15041475pub const ConnectError = ConnectErrorPartial || RequestError;
15051476
15061477/// Connect to `host:port` using the specified protocol. This will reuse a
......@@ -1548,7 +1519,7 @@ pub fn connect(
15481519pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError ||
15491520 std.fmt.ParseIntError || Connection.WriteError ||
15501521 error{ // TODO: file a zig fmt issue for this bad indentation
1551 UnsupportedUrlScheme,
1522 UnsupportedUriScheme,
15521523 UriMissingHost,
15531524
15541525 CertificateBundleLoadFailure,
......@@ -1598,12 +1569,25 @@ pub const RequestOptions = struct {
15981569 privileged_headers: []const http.Header = &.{},
15991570};
16001571
1601pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
1602 .{ "http", .plain },
1603 .{ "ws", .plain },
1604 .{ "https", .tls },
1605 .{ "wss", .tls },
1606});
1572fn validateUri(uri: Uri, arena: Allocator) !struct { Connection.Protocol, Uri } {
1573 const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
1574 .{ "http", .plain },
1575 .{ "ws", .plain },
1576 .{ "https", .tls },
1577 .{ "wss", .tls },
1578 });
1579 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUriScheme;
1580 var valid_uri = uri;
1581 // The host is always going to be needed as a raw string for hostname resolution anyway.
1582 valid_uri.host = .{
1583 .raw = try (uri.host orelse return error.UriMissingHost).toRawMaybeAlloc(arena),
1584 };
1585 valid_uri.port = uri.port orelse switch (protocol) {
1586 .plain => 80,
1587 .tls => 443,
1588 };
1589 return .{ protocol, valid_uri };
1590}
16071591
16081592/// Open a connection to the host specified by `uri` and prepare to send a HTTP request.
16091593///
......@@ -1633,14 +1617,8 @@ pub fn open(
16331617 }
16341618 }
16351619
1636 const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
1637
1638 const port: u16 = uri.port orelse switch (protocol) {
1639 .plain => 80,
1640 .tls => 443,
1641 };
1642
1643 const host = uri.host orelse return error.UriMissingHost;
1620 var server_header = std.heap.FixedBufferAllocator.init(options.server_header_buffer);
1621 const protocol, const valid_uri = try validateUri(uri, server_header.allocator());
16441622
16451623 if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .acquire)) {
16461624 if (disable_tls) unreachable;
......@@ -1649,15 +1627,17 @@ pub fn open(
16491627 defer client.ca_bundle_mutex.unlock();
16501628
16511629 if (client.next_https_rescan_certs) {
1652 client.ca_bundle.rescan(client.allocator) catch return error.CertificateBundleLoadFailure;
1630 client.ca_bundle.rescan(client.allocator) catch
1631 return error.CertificateBundleLoadFailure;
16531632 @atomicStore(bool, &client.next_https_rescan_certs, false, .release);
16541633 }
16551634 }
16561635
1657 const conn = options.connection orelse try client.connect(host, port, protocol);
1636 const conn = options.connection orelse
1637 try client.connect(valid_uri.host.?.raw, valid_uri.port.?, protocol);
16581638
16591639 var req: Request = .{
1660 .uri = uri,
1640 .uri = valid_uri,
16611641 .client = client,
16621642 .connection = conn,
16631643 .keep_alive = options.keep_alive,
......@@ -1671,7 +1651,7 @@ pub fn open(
16711651 .status = undefined,
16721652 .reason = undefined,
16731653 .keep_alive = undefined,
1674 .parser = proto.HeadersParser.init(options.server_header_buffer),
1654 .parser = proto.HeadersParser.init(server_header.buffer[server_header.end_index..]),
16751655 },
16761656 .headers = options.headers,
16771657 .extra_headers = options.extra_headers,
......@@ -1751,7 +1731,7 @@ pub fn fetch(client: *Client, options: FetchOptions) !FetchResult {
17511731
17521732 if (options.payload) |payload| req.transfer_encoding = .{ .content_length = payload.len };
17531733
1754 try req.send(.{ .raw_uri = options.raw_uri });
1734 try req.send();
17551735
17561736 if (options.payload) |payload| try req.writeAll(payload);
17571737
lib/std/http/test.zig+51-21
......@@ -64,7 +64,7 @@ test "trailers" {
6464 });
6565 defer req.deinit();
6666
67 try req.send(.{});
67 try req.send();
6868 try req.wait();
6969
7070 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -474,6 +474,15 @@ test "general client/server API coverage" {
474474 .{ .name = "location", .value = "/redirect/3" },
475475 },
476476 });
477 } else if (mem.eql(u8, request.head.target, "/redirect/5")) {
478 try request.respond("Hello, Redirected!\n", .{
479 .status = .found,
480 .extra_headers = &.{
481 .{ .name = "location", .value = "/%2525" },
482 },
483 });
484 } else if (mem.eql(u8, request.head.target, "/%2525")) {
485 try request.respond("Encoded redirect successful!\n", .{});
477486 } else if (mem.eql(u8, request.head.target, "/redirect/invalid")) {
478487 const invalid_port = try getUnusedTcpPort();
479488 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}", .{invalid_port});
......@@ -529,7 +538,7 @@ test "general client/server API coverage" {
529538 });
530539 defer req.deinit();
531540
532 try req.send(.{});
541 try req.send();
533542 try req.wait();
534543
535544 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -554,7 +563,7 @@ test "general client/server API coverage" {
554563 });
555564 defer req.deinit();
556565
557 try req.send(.{});
566 try req.send();
558567 try req.wait();
559568
560569 const body = try req.reader().readAllAlloc(gpa, 8192 * 1024);
......@@ -578,7 +587,7 @@ test "general client/server API coverage" {
578587 });
579588 defer req.deinit();
580589
581 try req.send(.{});
590 try req.send();
582591 try req.wait();
583592
584593 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -604,7 +613,7 @@ test "general client/server API coverage" {
604613 });
605614 defer req.deinit();
606615
607 try req.send(.{});
616 try req.send();
608617 try req.wait();
609618
610619 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -629,7 +638,7 @@ test "general client/server API coverage" {
629638 });
630639 defer req.deinit();
631640
632 try req.send(.{});
641 try req.send();
633642 try req.wait();
634643
635644 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -656,7 +665,7 @@ test "general client/server API coverage" {
656665 });
657666 defer req.deinit();
658667
659 try req.send(.{});
668 try req.send();
660669 try req.wait();
661670
662671 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -684,7 +693,7 @@ test "general client/server API coverage" {
684693 });
685694 defer req.deinit();
686695
687 try req.send(.{});
696 try req.send();
688697 try req.wait();
689698
690699 try std.testing.expectEqual(.ok, req.response.status);
......@@ -725,7 +734,7 @@ test "general client/server API coverage" {
725734 });
726735 defer req.deinit();
727736
728 try req.send(.{});
737 try req.send();
729738 try req.wait();
730739
731740 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -749,7 +758,7 @@ test "general client/server API coverage" {
749758 });
750759 defer req.deinit();
751760
752 try req.send(.{});
761 try req.send();
753762 try req.wait();
754763
755764 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -773,7 +782,7 @@ test "general client/server API coverage" {
773782 });
774783 defer req.deinit();
775784
776 try req.send(.{});
785 try req.send();
777786 try req.wait();
778787
779788 const body = try req.reader().readAllAlloc(gpa, 8192);
......@@ -797,13 +806,34 @@ test "general client/server API coverage" {
797806 });
798807 defer req.deinit();
799808
800 try req.send(.{});
809 try req.send();
801810 req.wait() catch |err| switch (err) {
802811 error.TooManyHttpRedirects => {},
803812 else => return err,
804813 };
805814 }
806815
816 { // redirect to encoded url
817 const location = try std.fmt.allocPrint(gpa, "http://127.0.0.1:{d}/redirect/5", .{port});
818 defer gpa.free(location);
819 const uri = try std.Uri.parse(location);
820
821 log.info("{s}", .{location});
822 var server_header_buffer: [1024]u8 = undefined;
823 var req = try client.open(.GET, uri, .{
824 .server_header_buffer = &server_header_buffer,
825 });
826 defer req.deinit();
827
828 try req.send();
829 try req.wait();
830
831 const body = try req.reader().readAllAlloc(gpa, 8192);
832 defer gpa.free(body);
833
834 try expectEqualStrings("Encoded redirect successful!\n", body);
835 }
836
807837 // connection has been kept alive
808838 try expect(client.http_proxy != null or client.connection_pool.free_len == 1);
809839
......@@ -819,7 +849,7 @@ test "general client/server API coverage" {
819849 });
820850 defer req.deinit();
821851
822 try req.send(.{});
852 try req.send();
823853 const result = req.wait();
824854
825855 // a proxy without an upstream is likely to return a 5xx status.
......@@ -913,16 +943,16 @@ test "Server streams both reading and writing" {
913943 var server_header_buffer: [555]u8 = undefined;
914944 var req = try client.open(.POST, .{
915945 .scheme = "http",
916 .host = "127.0.0.1",
946 .host = .{ .raw = "127.0.0.1" },
917947 .port = test_server.port(),
918 .path = "/",
948 .path = .{ .percent_encoded = "/" },
919949 }, .{
920950 .server_header_buffer = &server_header_buffer,
921951 });
922952 defer req.deinit();
923953
924954 req.transfer_encoding = .chunked;
925 try req.send(.{});
955 try req.send();
926956 try req.wait();
927957
928958 try req.writeAll("one ");
......@@ -956,7 +986,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
956986
957987 req.transfer_encoding = .{ .content_length = 14 };
958988
959 try req.send(.{});
989 try req.send();
960990 try req.writeAll("Hello, ");
961991 try req.writeAll("World!\n");
962992 try req.finish();
......@@ -990,7 +1020,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
9901020
9911021 req.transfer_encoding = .chunked;
9921022
993 try req.send(.{});
1023 try req.send();
9941024 try req.writeAll("Hello, ");
9951025 try req.writeAll("World!\n");
9961026 try req.finish();
......@@ -1044,7 +1074,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
10441074
10451075 req.transfer_encoding = .chunked;
10461076
1047 try req.send(.{});
1077 try req.send();
10481078 try req.writeAll("Hello, ");
10491079 try req.writeAll("World!\n");
10501080 try req.finish();
......@@ -1075,7 +1105,7 @@ fn echoTests(client: *http.Client, port: u16) !void {
10751105
10761106 req.transfer_encoding = .chunked;
10771107
1078 try req.send(.{});
1108 try req.send();
10791109 try req.wait();
10801110 try expectEqual(.expectation_failed, req.response.status);
10811111 }
......@@ -1180,7 +1210,7 @@ test "redirect to different connection" {
11801210 });
11811211 defer req.deinit();
11821212
1183 try req.send(.{});
1213 try req.send();
11841214 try req.wait();
11851215
11861216 const body = try req.reader().readAllAlloc(gpa, 8192);
lib/std/io.zig+1-1
......@@ -413,7 +413,7 @@ pub const StreamSource = @import("io/stream_source.zig").StreamSource;
413413pub const tty = @import("io/tty.zig");
414414
415415/// A Writer that doesn't write to anything.
416pub const null_writer = @as(NullWriter, .{ .context = {} });
416pub const null_writer: NullWriter = .{ .context = {} };
417417
418418const NullWriter = Writer(void, error{}, dummyWrite);
419419fn dummyWrite(context: void, data: []const u8) error{}!usize {
src/Package/Fetch.zig+13-11
......@@ -339,12 +339,12 @@ pub fn run(f: *Fetch) RunError!void {
339339 .path_or_url => |path_or_url| {
340340 if (fs.cwd().openDir(path_or_url, .{ .iterate = true })) |dir| {
341341 var resource: Resource = .{ .dir = dir };
342 return runResource(f, path_or_url, &resource, null);
342 return f.runResource(path_or_url, &resource, null);
343343 } else |dir_err| {
344344 const file_err = if (dir_err == error.NotDir) e: {
345345 if (fs.cwd().openFile(path_or_url, .{})) |file| {
346346 var resource: Resource = .{ .file = file };
347 return runResource(f, path_or_url, &resource, null);
347 return f.runResource(path_or_url, &resource, null);
348348 } else |err| break :e err;
349349 } else dir_err;
350350
......@@ -356,7 +356,7 @@ pub fn run(f: *Fetch) RunError!void {
356356 };
357357 var server_header_buffer: [header_buffer_size]u8 = undefined;
358358 var resource = try f.initResource(uri, &server_header_buffer);
359 return runResource(f, uri.path, &resource, null);
359 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, null);
360360 }
361361 },
362362 };
......@@ -418,7 +418,7 @@ pub fn run(f: *Fetch) RunError!void {
418418 );
419419 var server_header_buffer: [header_buffer_size]u8 = undefined;
420420 var resource = try f.initResource(uri, &server_header_buffer);
421 return runResource(f, uri.path, &resource, remote.hash);
421 return f.runResource(try uri.path.toRawMaybeAlloc(arena), &resource, remote.hash);
422422}
423423
424424pub fn deinit(f: *Fetch) void {
......@@ -897,13 +897,14 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
897897 const arena = f.arena.allocator();
898898 const eb = &f.error_bundle;
899899
900 if (ascii.eqlIgnoreCase(uri.scheme, "file")) return .{
901 .file = f.parent_package_root.openFile(uri.path, .{}) catch |err| {
900 if (ascii.eqlIgnoreCase(uri.scheme, "file")) {
901 const path = try uri.path.toRawMaybeAlloc(arena);
902 return .{ .file = f.parent_package_root.openFile(path, .{}) catch |err| {
902903 return f.fail(f.location_tok, try eb.printString("unable to open '{}{s}': {s}", .{
903 f.parent_package_root, uri.path, @errorName(err),
904 f.parent_package_root, path, @errorName(err),
904905 }));
905 },
906 };
906 } };
907 }
907908
908909 const http_client = f.job_queue.http_client;
909910
......@@ -920,7 +921,7 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
920921 };
921922 errdefer req.deinit(); // releases more than memory
922923
923 req.send(.{}) catch |err| {
924 req.send() catch |err| {
924925 return f.fail(f.location_tok, try eb.printString(
925926 "HTTP request failed: {s}",
926927 .{@errorName(err)},
......@@ -967,7 +968,8 @@ fn initResource(f: *Fetch, uri: std.Uri, server_header_buffer: []u8) RunError!Re
967968 };
968969
969970 const want_oid = want_oid: {
970 const want_ref = uri.fragment orelse "HEAD";
971 const want_ref =
972 if (uri.fragment) |fragment| try fragment.toRawMaybeAlloc(arena) else "HEAD";
971973 if (git.parseOid(want_ref)) |oid| break :want_oid oid else |_| {}
972974
973975 const want_ref_head = try std.fmt.allocPrint(arena, "refs/heads/{s}", .{want_ref});
src/Package/Fetch/git.zig+26-12
......@@ -540,9 +540,13 @@ pub const Session = struct {
540540 http_headers_buffer: []u8,
541541 ) !CapabilityIterator {
542542 var info_refs_uri = session.uri;
543 info_refs_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "info/refs" });
544 defer allocator.free(info_refs_uri.path);
545 info_refs_uri.query = "service=git-upload-pack";
543 {
544 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
545 defer allocator.free(session_uri_path);
546 info_refs_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "info/refs" }) };
547 }
548 defer allocator.free(info_refs_uri.path.percent_encoded);
549 info_refs_uri.query = .{ .percent_encoded = "service=git-upload-pack" };
546550 info_refs_uri.fragment = null;
547551
548552 const max_redirects = 3;
......@@ -554,16 +558,18 @@ pub const Session = struct {
554558 },
555559 });
556560 errdefer request.deinit();
557 try request.send(.{});
561 try request.send();
558562 try request.finish();
559563
560564 try request.wait();
561565 if (request.response.status != .ok) return error.ProtocolError;
562566 const any_redirects_occurred = request.redirect_behavior.remaining() < max_redirects;
563567 if (any_redirects_occurred) {
564 if (!mem.endsWith(u8, request.uri.path, "/info/refs")) return error.UnparseableRedirect;
568 const request_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{request.uri.path});
569 defer allocator.free(request_uri_path);
570 if (!mem.endsWith(u8, request_uri_path, "/info/refs")) return error.UnparseableRedirect;
565571 var new_uri = request.uri;
566 new_uri.path = new_uri.path[0 .. new_uri.path.len - "/info/refs".len];
572 new_uri.path = .{ .percent_encoded = request_uri_path[0 .. request_uri_path.len - "/info/refs".len] };
567573 new_uri.query = null;
568574 redirect_uri.* = try std.fmt.allocPrint(allocator, "{+/}", .{new_uri});
569575 return error.Redirected;
......@@ -645,8 +651,12 @@ pub const Session = struct {
645651 /// Returns an iterator over refs known to the server.
646652 pub fn listRefs(session: Session, allocator: Allocator, options: ListRefsOptions) !RefIterator {
647653 var upload_pack_uri = session.uri;
648 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
649 defer allocator.free(upload_pack_uri.path);
654 {
655 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
656 defer allocator.free(session_uri_path);
657 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
658 }
659 defer allocator.free(upload_pack_uri.path.percent_encoded);
650660 upload_pack_uri.query = null;
651661 upload_pack_uri.fragment = null;
652662
......@@ -681,7 +691,7 @@ pub const Session = struct {
681691 });
682692 errdefer request.deinit();
683693 request.transfer_encoding = .{ .content_length = body.items.len };
684 try request.send(.{});
694 try request.send();
685695 try request.writeAll(body.items);
686696 try request.finish();
687697
......@@ -748,8 +758,12 @@ pub const Session = struct {
748758 http_headers_buffer: []u8,
749759 ) !FetchStream {
750760 var upload_pack_uri = session.uri;
751 upload_pack_uri.path = try std.fs.path.resolvePosix(allocator, &.{ "/", session.uri.path, "git-upload-pack" });
752 defer allocator.free(upload_pack_uri.path);
761 {
762 const session_uri_path = try std.fmt.allocPrint(allocator, "{path}", .{session.uri.path});
763 defer allocator.free(session_uri_path);
764 upload_pack_uri.path = .{ .percent_encoded = try std.fs.path.resolvePosix(allocator, &.{ "/", session_uri_path, "git-upload-pack" }) };
765 }
766 defer allocator.free(upload_pack_uri.path.percent_encoded);
753767 upload_pack_uri.query = null;
754768 upload_pack_uri.fragment = null;
755769
......@@ -786,7 +800,7 @@ pub const Session = struct {
786800 });
787801 errdefer request.deinit();
788802 request.transfer_encoding = .{ .content_length = body.items.len };
789 try request.send(.{});
803 try request.send();
790804 try request.writeAll(body.items);
791805 try request.finish();
792806
src/link/SpirV.zig+12-3
......@@ -233,9 +233,18 @@ pub fn flushModule(self: *SpirV, arena: Allocator, prog_node: *std.Progress.Node
233233 // name if it contains no strange characters is nice for debugging. URI encoding fits the bill.
234234 // We're using : as separator, which is a reserved character.
235235
236 const escaped_name = try std.Uri.escapeString(gpa, name.toSlice(&mod.intern_pool));
237 defer gpa.free(escaped_name);
238 try error_info.writer().print(":{s}", .{escaped_name});
236 try std.Uri.Component.percentEncode(
237 error_info.writer(),
238 name.toSlice(&mod.intern_pool),
239 struct {
240 fn isValidChar(c: u8) bool {
241 return switch (c) {
242 0, '%', ':' => false,
243 else => true,
244 };
245 }
246 }.isValidChar,
247 );
239248 }
240249 try spv.sections.debug_strings.emit(gpa, .OpSourceExtension, .{
241250 .extension = error_info.items,