| author | |
| committer | |
| log | d4545f216a6ea86caec208cabc724004b7ef320a |
| tree | 75a826b42293794f3ab9341703a11d8c6cbd926c |
| parent | 7d6b5ed5103e6a06d1bd8be884f6c2a7ecb0e09a |
rely on the field parent pointer pattern7 files changed, 221 insertions(+), 189 deletions(-)
lib/std/crypto/tls/Client.zig+51-51| ... | ... | @@ -21,11 +21,15 @@ const array = tls.array; |
| 21 | 21 | /// here via `reader`. |
| 22 | 22 | /// |
| 23 | 23 | /// The buffer is asserted to have capacity at least `min_buffer_len`. |
| 24 | input: *std.io.Reader, | |
| 24 | input: *Reader, | |
| 25 | /// Decrypted stream from the server to the client. | |
| 26 | reader: Reader, | |
| 25 | 27 | |
| 26 | 28 | /// The encrypted stream from the client to the server. Bytes are pushed here |
| 27 | 29 | /// via `writer`. |
| 28 | 30 | output: *Writer, |
| 31 | /// The plaintext stream from the client to the server. | |
| 32 | writer: Writer, | |
| 29 | 33 | |
| 30 | 34 | /// Populated when `error.TlsAlert` is returned. |
| 31 | 35 | alert: ?tls.Alert = null, |
| ... | ... | @@ -36,14 +40,6 @@ write_seq: u64, |
| 36 | 40 | /// When this is true, the stream may still not be at the end because there |
| 37 | 41 | /// may be data in the input buffer. |
| 38 | 42 | received_close_notify: bool, |
| 39 | /// By default, reaching the end-of-stream when reading from the server will | |
| 40 | /// cause `error.TlsConnectionTruncated` to be returned, unless a close_notify | |
| 41 | /// message has been received. By setting this flag to `true`, instead, the | |
| 42 | /// end-of-stream will be forwarded to the application layer above TLS. | |
| 43 | /// | |
| 44 | /// This makes the application vulnerable to truncation attacks unless the | |
| 45 | /// application layer itself verifies that the amount of data received equals | |
| 46 | /// the amount of data expected, such as HTTP with the Content-Length header. | |
| 47 | 43 | allow_truncation_attacks: bool, |
| 48 | 44 | application_cipher: tls.ApplicationCipher, |
| 49 | 45 | |
| ... | ... | @@ -85,7 +81,7 @@ pub const SslKeyLog = struct { |
| 85 | 81 | } |
| 86 | 82 | }; |
| 87 | 83 | |
| 88 | /// The `std.io.Reader` supplied to `init` requires a buffer capacity | |
| 84 | /// The `Reader` supplied to `init` requires a buffer capacity | |
| 89 | 85 | /// at least this amount. |
| 90 | 86 | pub const min_buffer_len = tls.max_ciphertext_record_len; |
| 91 | 87 | |
| ... | ... | @@ -116,6 +112,20 @@ pub const Options = struct { |
| 116 | 112 | /// Only the `writer` field is observed during the handshake (`init`). |
| 117 | 113 | /// After that, the other fields are populated. |
| 118 | 114 | ssl_key_log: ?*SslKeyLog = null, |
| 115 | /// By default, reaching the end-of-stream when reading from the server will | |
| 116 | /// cause `error.TlsConnectionTruncated` to be returned, unless a close_notify | |
| 117 | /// message has been received. By setting this flag to `true`, instead, the | |
| 118 | /// end-of-stream will be forwarded to the application layer above TLS. | |
| 119 | /// | |
| 120 | /// This makes the application vulnerable to truncation attacks unless the | |
| 121 | /// application layer itself verifies that the amount of data received equals | |
| 122 | /// the amount of data expected, such as HTTP with the Content-Length header. | |
| 123 | allow_truncation_attacks: bool = false, | |
| 124 | write_buffer: []u8, | |
| 125 | /// Asserted to have capacity at least `min_buffer_len`. | |
| 126 | read_buffer: []u8, | |
| 127 | /// Populated when `error.TlsAlert` is returned from `init`. | |
| 128 | alert: ?*tls.Alert = null, | |
| 119 | 129 | }; |
| 120 | 130 | |
| 121 | 131 | const InitError = error{ |
| ... | ... | @@ -173,14 +183,8 @@ const InitError = error{ |
| 173 | 183 | /// `host` is only borrowed during this function call. |
| 174 | 184 | /// |
| 175 | 185 | /// `input` is asserted to have buffer capacity at least `min_buffer_len`. |
| 176 | pub fn init( | |
| 177 | client: *Client, | |
| 178 | input: *std.io.Reader, | |
| 179 | output: *Writer, | |
| 180 | options: Options, | |
| 181 | ) InitError!void { | |
| 186 | pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client { | |
| 182 | 187 | assert(input.buffer.len >= min_buffer_len); |
| 183 | client.alert = null; | |
| 184 | 188 | const host = switch (options.host) { |
| 185 | 189 | .no_verification => "", |
| 186 | 190 | .explicit => |host| host, |
| ... | ... | @@ -411,7 +415,7 @@ pub fn init( |
| 411 | 415 | switch (ct) { |
| 412 | 416 | .alert => { |
| 413 | 417 | ctd.ensure(2) catch continue :fragment; |
| 414 | client.alert = .{ | |
| 418 | if (options.alert) |a| a.* = .{ | |
| 415 | 419 | .level = ctd.decode(tls.Alert.Level), |
| 416 | 420 | .description = ctd.decode(tls.Alert.Description), |
| 417 | 421 | }; |
| ... | ... | @@ -852,9 +856,28 @@ pub fn init( |
| 852 | 856 | else => unreachable, |
| 853 | 857 | }, |
| 854 | 858 | }; |
| 855 | client.* = .{ | |
| 859 | if (options.ssl_key_log) |ssl_key_log| ssl_key_log.* = .{ | |
| 860 | .client_key_seq = key_seq, | |
| 861 | .server_key_seq = key_seq, | |
| 862 | .client_random = client_hello_rand, | |
| 863 | .writer = ssl_key_log.writer, | |
| 864 | }; | |
| 865 | return .{ | |
| 856 | 866 | .input = input, |
| 867 | .reader = .{ | |
| 868 | .buffer = options.read_buffer, | |
| 869 | .vtable = &.{ .stream = stream }, | |
| 870 | .seek = 0, | |
| 871 | .end = 0, | |
| 872 | }, | |
| 857 | 873 | .output = output, |
| 874 | .writer = .{ | |
| 875 | .buffer = options.write_buffer, | |
| 876 | .vtable = &.{ | |
| 877 | .drain = drain, | |
| 878 | .sendFile = Writer.unimplementedSendFile, | |
| 879 | }, | |
| 880 | }, | |
| 858 | 881 | .tls_version = tls_version, |
| 859 | 882 | .read_seq = switch (tls_version) { |
| 860 | 883 | .tls_1_3 => 0, |
| ... | ... | @@ -867,17 +890,10 @@ pub fn init( |
| 867 | 890 | else => unreachable, |
| 868 | 891 | }, |
| 869 | 892 | .received_close_notify = false, |
| 870 | .allow_truncation_attacks = false, | |
| 893 | .allow_truncation_attacks = options.allow_truncation_attacks, | |
| 871 | 894 | .application_cipher = app_cipher, |
| 872 | 895 | .ssl_key_log = options.ssl_key_log, |
| 873 | 896 | }; |
| 874 | if (options.ssl_key_log) |ssl_key_log| ssl_key_log.* = .{ | |
| 875 | .client_key_seq = key_seq, | |
| 876 | .server_key_seq = key_seq, | |
| 877 | .client_random = client_hello_rand, | |
| 878 | .writer = ssl_key_log.writer, | |
| 879 | }; | |
| 880 | return; | |
| 881 | 897 | }, |
| 882 | 898 | else => return error.TlsUnexpectedMessage, |
| 883 | 899 | } |
| ... | ... | @@ -891,25 +907,9 @@ pub fn init( |
| 891 | 907 | } |
| 892 | 908 | } |
| 893 | 909 | |
| 894 | pub fn reader(c: *Client) Reader { | |
| 895 | return .{ | |
| 896 | .context = c, | |
| 897 | .vtable = &.{ .read = read }, | |
| 898 | }; | |
| 899 | } | |
| 900 | ||
| 901 | pub fn writer(c: *Client) Writer { | |
| 902 | return .{ | |
| 903 | .context = c, | |
| 904 | .vtable = &.{ | |
| 905 | .writeSplat = writeSplat, | |
| 906 | .writeFile = Writer.unimplementedWriteFile, | |
| 907 | }, | |
| 908 | }; | |
| 909 | } | |
| 910 | ||
| 911 | fn writeSplat(context: ?*anyopaque, data: []const []const u8, splat: usize) Writer.Error!usize { | |
| 912 | const c: *Client = @alignCast(@ptrCast(context)); | |
| 910 | fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize { | |
| 911 | const c: *Client = @fieldParentPtr("writer", w); | |
| 912 | if (true) @panic("update to use the buffer and flush"); | |
| 913 | 913 | const sliced_data = if (splat == 0) data[0..data.len -| 1] else data; |
| 914 | 914 | const output = c.output; |
| 915 | 915 | const ciphertext_buf = try output.writableSliceGreedy(min_buffer_len); |
| ... | ... | @@ -1043,8 +1043,8 @@ pub fn eof(c: Client) bool { |
| 1043 | 1043 | return c.received_close_notify; |
| 1044 | 1044 | } |
| 1045 | 1045 | |
| 1046 | fn read(context: ?*anyopaque, bw: *Writer, limit: std.io.Limit) Reader.StreamError!usize { | |
| 1047 | const c: *Client = @ptrCast(@alignCast(context)); | |
| 1046 | fn stream(r: *Reader, w: *Writer, limit: std.io.Limit) Reader.StreamError!usize { | |
| 1047 | const c: *Client = @fieldParentPtr("reader", r); | |
| 1048 | 1048 | if (c.eof()) return error.EndOfStream; |
| 1049 | 1049 | const input = c.input; |
| 1050 | 1050 | // If at least one full encrypted record is not buffered, read once. |
| ... | ... | @@ -1214,7 +1214,7 @@ fn read(context: ?*anyopaque, bw: *Writer, limit: std.io.Limit) Reader.StreamErr |
| 1214 | 1214 | }, |
| 1215 | 1215 | .application_data => { |
| 1216 | 1216 | if (@intFromEnum(limit) < cleartext.len) return failRead(c, error.OutputBufferUndersize); |
| 1217 | try bw.writeAll(cleartext); | |
| 1217 | try w.writeAll(cleartext); | |
| 1218 | 1218 | return cleartext.len; |
| 1219 | 1219 | }, |
| 1220 | 1220 | else => return failRead(c, error.TlsUnexpectedMessage), |
| ... | ... | @@ -1226,8 +1226,8 @@ fn failRead(c: *Client, err: ReadError) error{ReadFailed} { |
| 1226 | 1226 | return error.ReadFailed; |
| 1227 | 1227 | } |
| 1228 | 1228 | |
| 1229 | fn logSecrets(bw: *Writer, context: anytype, secrets: anytype) void { | |
| 1230 | inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| bw.print("{s}" ++ | |
| 1229 | fn logSecrets(w: *Writer, context: anytype, secrets: anytype) void { | |
| 1230 | inline for (@typeInfo(@TypeOf(secrets)).@"struct".fields) |field| w.print("{s}" ++ | |
| 1231 | 1231 | (if (@hasField(@TypeOf(context), "counter")) "_{d}" else "") ++ " {x} {x}\n", .{field.name} ++ |
| 1232 | 1232 | (if (@hasField(@TypeOf(context), "counter")) .{context.counter} else .{}) ++ .{ |
| 1233 | 1233 | context.client_random, |
lib/std/fs/File.zig-2| ... | ... | @@ -943,7 +943,6 @@ pub const Reader = struct { |
| 943 | 943 | |
| 944 | 944 | pub fn initInterface(buffer: []u8) std.io.Reader { |
| 945 | 945 | return .{ |
| 946 | .context = undefined, | |
| 947 | 946 | .vtable = &.{ |
| 948 | 947 | .stream = Reader.stream, |
| 949 | 948 | .discard = Reader.discard, |
| ... | ... | @@ -1291,7 +1290,6 @@ pub const Writer = struct { |
| 1291 | 1290 | |
| 1292 | 1291 | pub fn initInterface(buffer: []u8) std.io.Writer { |
| 1293 | 1292 | return .{ |
| 1294 | .context = undefined, | |
| 1295 | 1293 | .vtable = &.{ |
| 1296 | 1294 | .drain = drain, |
| 1297 | 1295 | .sendFile = sendFile, |
lib/std/http.zig+45-62| ... | ... | @@ -328,6 +328,7 @@ pub const Header = struct { |
| 328 | 328 | |
| 329 | 329 | pub const Reader = struct { |
| 330 | 330 | in: *std.io.Reader, |
| 331 | interface: std.io.Reader, | |
| 331 | 332 | /// Keeps track of whether the stream is ready to accept a new request, |
| 332 | 333 | /// making invalid API usage cause assertion failures rather than HTTP |
| 333 | 334 | /// protocol violations. |
| ... | ... | @@ -438,37 +439,41 @@ pub const Reader = struct { |
| 438 | 439 | buffer: []u8, |
| 439 | 440 | transfer_encoding: TransferEncoding, |
| 440 | 441 | content_length: ?u64, |
| 441 | ) std.io.Reader { | |
| 442 | ) *std.io.Reader { | |
| 442 | 443 | assert(reader.state == .received_head); |
| 443 | return switch (transfer_encoding) { | |
| 444 | switch (transfer_encoding) { | |
| 444 | 445 | .chunked => { |
| 445 | 446 | reader.state = .{ .body_remaining_chunk_len = .head }; |
| 446 | return .{ | |
| 447 | reader.interface = .{ | |
| 447 | 448 | .buffer = buffer, |
| 448 | .context = reader, | |
| 449 | .seek = 0, | |
| 450 | .end = 0, | |
| 449 | 451 | .vtable = &.{ |
| 450 | .read = chunkedRead, | |
| 452 | .stream = chunkedStream, | |
| 451 | 453 | .discard = chunkedDiscard, |
| 452 | 454 | }, |
| 453 | 455 | }; |
| 456 | return &reader.interface; | |
| 454 | 457 | }, |
| 455 | 458 | .none => { |
| 456 | 459 | if (content_length) |len| { |
| 457 | 460 | reader.state = .{ .body_remaining_content_length = len }; |
| 458 | return .{ | |
| 461 | reader.interface = .{ | |
| 459 | 462 | .buffer = buffer, |
| 460 | .context = reader, | |
| 463 | .seek = 0, | |
| 464 | .end = 0, | |
| 461 | 465 | .vtable = &.{ |
| 462 | .read = contentLengthRead, | |
| 466 | .stream = contentLengthStream, | |
| 463 | 467 | .discard = contentLengthDiscard, |
| 464 | 468 | }, |
| 465 | 469 | }; |
| 470 | return &reader.interface; | |
| 466 | 471 | } else { |
| 467 | 472 | reader.state = .body_none; |
| 468 | return reader.in.reader(); | |
| 473 | return reader.in; | |
| 469 | 474 | } |
| 470 | 475 | }, |
| 471 | }; | |
| 476 | } | |
| 472 | 477 | } |
| 473 | 478 | |
| 474 | 479 | /// If compressed body has been negotiated this will return decompressed bytes. |
| ... | ... | @@ -511,25 +516,25 @@ pub const Reader = struct { |
| 511 | 516 | return decompressor.reader(transfer_reader, decompression_buffer, content_encoding); |
| 512 | 517 | } |
| 513 | 518 | |
| 514 | fn contentLengthRead( | |
| 515 | ctx: ?*anyopaque, | |
| 516 | bw: *Writer, | |
| 519 | fn contentLengthStream( | |
| 520 | io_r: *std.io.Reader, | |
| 521 | w: *Writer, | |
| 517 | 522 | limit: std.io.Limit, |
| 518 | 523 | ) std.io.Reader.StreamError!usize { |
| 519 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | |
| 524 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 520 | 525 | const remaining_content_length = &reader.state.body_remaining_content_length; |
| 521 | 526 | const remaining = remaining_content_length.*; |
| 522 | 527 | if (remaining == 0) { |
| 523 | 528 | reader.state = .ready; |
| 524 | 529 | return error.EndOfStream; |
| 525 | 530 | } |
| 526 | const n = try reader.in.read(bw, limit.min(.limited(remaining))); | |
| 531 | const n = try reader.in.stream(w, limit.min(.limited(remaining))); | |
| 527 | 532 | remaining_content_length.* = remaining - n; |
| 528 | 533 | return n; |
| 529 | 534 | } |
| 530 | 535 | |
| 531 | fn contentLengthDiscard(ctx: ?*anyopaque, limit: std.io.Limit) std.io.Reader.Error!usize { | |
| 532 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | |
| 536 | fn contentLengthDiscard(io_r: *std.io.Reader, limit: std.io.Limit) std.io.Reader.Error!usize { | |
| 537 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 533 | 538 | const remaining_content_length = &reader.state.body_remaining_content_length; |
| 534 | 539 | const remaining = remaining_content_length.*; |
| 535 | 540 | if (remaining == 0) { |
| ... | ... | @@ -541,18 +546,14 @@ pub const Reader = struct { |
| 541 | 546 | return n; |
| 542 | 547 | } |
| 543 | 548 | |
| 544 | fn chunkedRead( | |
| 545 | ctx: ?*anyopaque, | |
| 546 | bw: *Writer, | |
| 547 | limit: std.io.Limit, | |
| 548 | ) std.io.Reader.StreamError!usize { | |
| 549 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | |
| 549 | fn chunkedStream(io_r: *std.io.Reader, w: *Writer, limit: std.io.Limit) std.io.Reader.StreamError!usize { | |
| 550 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 550 | 551 | const chunk_len_ptr = switch (reader.state) { |
| 551 | 552 | .ready => return error.EndOfStream, |
| 552 | 553 | .body_remaining_chunk_len => |*x| x, |
| 553 | 554 | else => unreachable, |
| 554 | 555 | }; |
| 555 | return chunkedReadEndless(reader, bw, limit, chunk_len_ptr) catch |err| switch (err) { | |
| 556 | return chunkedReadEndless(reader, w, limit, chunk_len_ptr) catch |err| switch (err) { | |
| 556 | 557 | error.ReadFailed => return error.ReadFailed, |
| 557 | 558 | error.WriteFailed => return error.WriteFailed, |
| 558 | 559 | error.EndOfStream => { |
| ... | ... | @@ -568,7 +569,7 @@ pub const Reader = struct { |
| 568 | 569 | |
| 569 | 570 | fn chunkedReadEndless( |
| 570 | 571 | reader: *Reader, |
| 571 | bw: *Writer, | |
| 572 | w: *Writer, | |
| 572 | 573 | limit: std.io.Limit, |
| 573 | 574 | chunk_len_ptr: *RemainingChunkLen, |
| 574 | 575 | ) (BodyError || std.io.Reader.StreamError)!usize { |
| ... | ... | @@ -592,7 +593,7 @@ pub const Reader = struct { |
| 592 | 593 | } |
| 593 | 594 | } |
| 594 | 595 | if (cp.chunk_len == 0) return parseTrailers(reader, 0); |
| 595 | const n = try in.read(bw, limit.min(.limited(cp.chunk_len))); | |
| 596 | const n = try in.stream(w, limit.min(.limited(cp.chunk_len))); | |
| 596 | 597 | chunk_len_ptr.* = .init(cp.chunk_len + 2 - n); |
| 597 | 598 | return n; |
| 598 | 599 | }, |
| ... | ... | @@ -608,15 +609,15 @@ pub const Reader = struct { |
| 608 | 609 | continue :len .head; |
| 609 | 610 | }, |
| 610 | 611 | else => |remaining_chunk_len| { |
| 611 | const n = try in.read(bw, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); | |
| 612 | const n = try in.stream(w, limit.min(.limited(@intFromEnum(remaining_chunk_len) - 2))); | |
| 612 | 613 | chunk_len_ptr.* = .init(@intFromEnum(remaining_chunk_len) - n); |
| 613 | 614 | return n; |
| 614 | 615 | }, |
| 615 | 616 | } |
| 616 | 617 | } |
| 617 | 618 | |
| 618 | fn chunkedDiscard(ctx: ?*anyopaque, limit: std.io.Limit) std.io.Reader.Error!usize { | |
| 619 | const reader: *Reader = @alignCast(@ptrCast(ctx)); | |
| 619 | fn chunkedDiscard(io_r: *std.io.Reader, limit: std.io.Limit) std.io.Reader.Error!usize { | |
| 620 | const reader: *Reader = @fieldParentPtr("interface", io_r); | |
| 620 | 621 | const chunk_len_ptr = switch (reader.state) { |
| 621 | 622 | .ready => return error.EndOfStream, |
| 622 | 623 | .body_remaining_chunk_len => |*x| x, |
| ... | ... | @@ -758,7 +759,6 @@ pub const BodyWriter = struct { |
| 758 | 759 | /// state of this other than via methods of `BodyWriter`. |
| 759 | 760 | http_protocol_output: *Writer, |
| 760 | 761 | state: State, |
| 761 | elide: bool, | |
| 762 | 762 | interface: Writer, |
| 763 | 763 | |
| 764 | 764 | pub const Error = Writer.Error; |
| ... | ... | @@ -796,6 +796,10 @@ pub const BodyWriter = struct { |
| 796 | 796 | }; |
| 797 | 797 | }; |
| 798 | 798 | |
| 799 | pub fn isEliding(w: *const BodyWriter) bool { | |
| 800 | return w.interface.vtable.drain == Writer.discardingDrain; | |
| 801 | } | |
| 802 | ||
| 799 | 803 | /// Sends all buffered data across `BodyWriter.http_protocol_output`. |
| 800 | 804 | pub fn flush(w: *BodyWriter) Error!void { |
| 801 | 805 | const out = w.http_protocol_output; |
| ... | ... | @@ -825,7 +829,7 @@ pub const BodyWriter = struct { |
| 825 | 829 | /// with empty trailers, then flushes the stream to the system. Asserts any |
| 826 | 830 | /// started chunk has been completely finished. |
| 827 | 831 | /// |
| 828 | /// Respects the value of `elide` to omit all data after the headers. | |
| 832 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 829 | 833 | /// |
| 830 | 834 | /// See also: |
| 831 | 835 | /// * `endUnflushed` |
| ... | ... | @@ -841,7 +845,7 @@ pub const BodyWriter = struct { |
| 841 | 845 | /// Otherwise, transfer-encoding: chunked is being used, and it writes the |
| 842 | 846 | /// end-of-stream message with empty trailers. |
| 843 | 847 | /// |
| 844 | /// Respects the value of `elide` to omit all data after the headers. | |
| 848 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 845 | 849 | /// |
| 846 | 850 | /// See also: |
| 847 | 851 | /// * `end` |
| ... | ... | @@ -867,7 +871,7 @@ pub const BodyWriter = struct { |
| 867 | 871 | /// |
| 868 | 872 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. |
| 869 | 873 | /// |
| 870 | /// Respects the value of `elide` to omit all data after the headers. | |
| 874 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 871 | 875 | /// |
| 872 | 876 | /// See also: |
| 873 | 877 | /// * `endChunkedUnflushed` |
| ... | ... | @@ -883,7 +887,7 @@ pub const BodyWriter = struct { |
| 883 | 887 | /// |
| 884 | 888 | /// Asserts that the BodyWriter is using transfer-encoding: chunked. |
| 885 | 889 | /// |
| 886 | /// Respects the value of `elide` to omit all data after the headers. | |
| 890 | /// Respects the value of `isEliding` to omit all data after the headers. | |
| 887 | 891 | /// |
| 888 | 892 | /// See also: |
| 889 | 893 | /// * `endChunked` |
| ... | ... | @@ -891,7 +895,7 @@ pub const BodyWriter = struct { |
| 891 | 895 | /// * `end` |
| 892 | 896 | pub fn endChunkedUnflushed(w: *BodyWriter, options: EndChunkedOptions) Error!void { |
| 893 | 897 | const chunked = &w.state.chunked; |
| 894 | if (w.elide) { | |
| 898 | if (w.isEliding()) { | |
| 895 | 899 | w.state = .end; |
| 896 | 900 | return; |
| 897 | 901 | } |
| ... | ... | @@ -922,7 +926,7 @@ pub const BodyWriter = struct { |
| 922 | 926 | |
| 923 | 927 | fn contentLengthDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { |
| 924 | 928 | const bw: *BodyWriter = @fieldParentPtr("interface", w); |
| 925 | assert(!bw.elide); | |
| 929 | assert(!bw.isEliding()); | |
| 926 | 930 | const out = w.http_protocol_output; |
| 927 | 931 | const n = try w.drainTo(out, data, splat); |
| 928 | 932 | w.state.content_length -= n; |
| ... | ... | @@ -931,7 +935,7 @@ pub const BodyWriter = struct { |
| 931 | 935 | |
| 932 | 936 | fn noneDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { |
| 933 | 937 | const bw: *BodyWriter = @fieldParentPtr("interface", w); |
| 934 | assert(!bw.elide); | |
| 938 | assert(!bw.isEliding()); | |
| 935 | 939 | const out = w.http_protocol_output; |
| 936 | 940 | return try w.drainTo(out, data, splat); |
| 937 | 941 | } |
| ... | ... | @@ -939,13 +943,13 @@ pub const BodyWriter = struct { |
| 939 | 943 | /// Returns `null` if size cannot be computed without making any syscalls. |
| 940 | 944 | fn noneSendFile(w: *Writer, file_reader: *File.Reader, limit: std.io.Limit) Writer.FileError!usize { |
| 941 | 945 | const bw: *BodyWriter = @fieldParentPtr("interface", w); |
| 942 | assert(!bw.elide); | |
| 946 | assert(!bw.isEliding()); | |
| 943 | 947 | return w.sendFileTo(bw.http_protocol_output, file_reader, limit); |
| 944 | 948 | } |
| 945 | 949 | |
| 946 | 950 | fn contentLengthSendFile(w: *Writer, file_reader: *File.Reader, limit: std.io.Limit) Writer.FileError!usize { |
| 947 | 951 | const bw: *BodyWriter = @fieldParentPtr("interface", w); |
| 948 | assert(!bw.elide); | |
| 952 | assert(!bw.isEliding()); | |
| 949 | 953 | const n = try w.sendFileTo(bw.http_protocol_output, file_reader, limit); |
| 950 | 954 | bw.state.content_length -= n; |
| 951 | 955 | return n; |
| ... | ... | @@ -953,7 +957,7 @@ pub const BodyWriter = struct { |
| 953 | 957 | |
| 954 | 958 | fn chunkedSendFile(w: *Writer, file_reader: *File.Reader, limit: std.io.Limit) Writer.FileError!usize { |
| 955 | 959 | const bw: *BodyWriter = @fieldParentPtr("interface", w); |
| 956 | assert(!bw.elide); | |
| 960 | assert(!bw.isEliding()); | |
| 957 | 961 | const data_len = w.countSendFileUpperBound(file_reader, limit) orelse { |
| 958 | 962 | // If the file size is unknown, we cannot lower to a `writeFile` since we would |
| 959 | 963 | // have to flush the chunk header before knowing the chunk length. |
| ... | ... | @@ -1001,7 +1005,7 @@ pub const BodyWriter = struct { |
| 1001 | 1005 | |
| 1002 | 1006 | fn chunkedDrain(w: *Writer, data: []const []const u8, splat: usize) Error!usize { |
| 1003 | 1007 | const bw: *BodyWriter = @fieldParentPtr("interface", w); |
| 1004 | assert(!bw.elide); | |
| 1008 | assert(!bw.isEliding()); | |
| 1005 | 1009 | const out = w.http_protocol_output; |
| 1006 | 1010 | const data_len = Writer.countSplat(w.end, data, splat); |
| 1007 | 1011 | const chunked = &bw.state.chunked; |
| ... | ... | @@ -1059,27 +1063,6 @@ pub const BodyWriter = struct { |
| 1059 | 1063 | a /= base; |
| 1060 | 1064 | } |
| 1061 | 1065 | } |
| 1062 | ||
| 1063 | pub fn writer(w: *BodyWriter) Writer { | |
| 1064 | return if (w.elide) .discarding else .{ | |
| 1065 | .context = w, | |
| 1066 | .vtable = switch (w.state) { | |
| 1067 | .none => &.{ | |
| 1068 | .drain = noneDrain, | |
| 1069 | .sendFile = noneSendFile, | |
| 1070 | }, | |
| 1071 | .content_length => &.{ | |
| 1072 | .drain = contentLengthDrain, | |
| 1073 | .sendFile = contentLengthSendFile, | |
| 1074 | }, | |
| 1075 | .chunked => &.{ | |
| 1076 | .drain = chunkedDrain, | |
| 1077 | .sendFile = chunkedSendFile, | |
| 1078 | }, | |
| 1079 | .end => unreachable, | |
| 1080 | }, | |
| 1081 | }; | |
| 1082 | } | |
| 1083 | 1066 | }; |
| 1084 | 1067 | |
| 1085 | 1068 | test { |
lib/std/http/Client.zig+92-50| ... | ... | @@ -14,6 +14,7 @@ const Uri = std.Uri; |
| 14 | 14 | const Allocator = mem.Allocator; |
| 15 | 15 | const assert = std.debug.assert; |
| 16 | 16 | const Writer = std.io.Writer; |
| 17 | const Reader = std.io.Reader; | |
| 17 | 18 | |
| 18 | 19 | const Client = @This(); |
| 19 | 20 | |
| ... | ... | @@ -228,12 +229,6 @@ pub const Connection = struct { |
| 228 | 229 | client: *Client, |
| 229 | 230 | stream_writer: net.Stream.Writer, |
| 230 | 231 | stream_reader: net.Stream.Reader, |
| 231 | /// HTTP protocol from client to server. | |
| 232 | /// This either goes directly to `stream_writer`, or to a TLS client. | |
| 233 | writer: Writer, | |
| 234 | /// HTTP protocol from server to client. | |
| 235 | /// This either comes directly from `stream_reader`, or from a TLS client. | |
| 236 | reader: std.io.Reader, | |
| 237 | 232 | /// Entry in `ConnectionPool.used` or `ConnectionPool.free`. |
| 238 | 233 | pool_node: std.DoublyLinkedList.Node, |
| 239 | 234 | port: u16, |
| ... | ... | @@ -264,10 +259,8 @@ pub const Connection = struct { |
| 264 | 259 | plain.* = .{ |
| 265 | 260 | .connection = .{ |
| 266 | 261 | .client = client, |
| 267 | .stream_writer = stream.writer(), | |
| 268 | .stream_reader = stream.reader(), | |
| 269 | .writer = plain.connection.stream_writer.interface().buffered(socket_write_buffer), | |
| 270 | .reader = plain.connection.stream_reader.interface().buffered(socket_read_buffer), | |
| 262 | .stream_writer = stream.writer(socket_write_buffer), | |
| 263 | .stream_reader = stream.reader(socket_read_buffer), | |
| 271 | 264 | .pool_node = .{}, |
| 272 | 265 | .port = port, |
| 273 | 266 | .host_len = @intCast(remote_host.len), |
| ... | ... | @@ -297,10 +290,6 @@ pub const Connection = struct { |
| 297 | 290 | }; |
| 298 | 291 | |
| 299 | 292 | const Tls = struct { |
| 300 | /// Data from `client` to `Connection.stream`. | |
| 301 | writer: Writer, | |
| 302 | /// Data from `Connection.stream` to `client`. | |
| 303 | reader: std.io.Reader, | |
| 304 | 293 | client: std.crypto.tls.Client, |
| 305 | 294 | connection: Connection, |
| 306 | 295 | |
| ... | ... | @@ -324,10 +313,8 @@ pub const Connection = struct { |
| 324 | 313 | tls.* = .{ |
| 325 | 314 | .connection = .{ |
| 326 | 315 | .client = client, |
| 327 | .stream_writer = stream.writer(), | |
| 328 | .stream_reader = stream.reader(), | |
| 329 | .writer = tls.client.writer().buffered(socket_write_buffer), | |
| 330 | .reader = tls.client.reader().unbuffered(), | |
| 316 | .stream_writer = stream.writer(socket_write_buffer), | |
| 317 | .stream_reader = stream.reader(&.{}), | |
| 331 | 318 | .pool_node = .{}, |
| 332 | 319 | .port = port, |
| 333 | 320 | .host_len = @intCast(remote_host.len), |
| ... | ... | @@ -335,20 +322,22 @@ pub const Connection = struct { |
| 335 | 322 | .closing = false, |
| 336 | 323 | .protocol = .tls, |
| 337 | 324 | }, |
| 338 | .writer = tls.connection.stream_writer.interface().buffered(tls_write_buffer), | |
| 339 | .reader = tls.connection.stream_reader.interface().buffered(tls_read_buffer), | |
| 340 | .client = undefined, | |
| 325 | // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true | |
| 326 | .client = std.crypto.tls.Client.init( | |
| 327 | tls.connection.stream_reader.interface(), | |
| 328 | &tls.connection.stream_writer.interface, | |
| 329 | .{ | |
| 330 | .host = .{ .explicit = remote_host }, | |
| 331 | .ca = .{ .bundle = client.ca_bundle }, | |
| 332 | .ssl_key_log = client.ssl_key_log, | |
| 333 | .read_buffer = tls_read_buffer, | |
| 334 | .write_buffer = tls_write_buffer, | |
| 335 | // This is appropriate for HTTPS because the HTTP headers contain | |
| 336 | // the content length which is used to detect truncation attacks. | |
| 337 | .allow_truncation_attacks = true, | |
| 338 | }, | |
| 339 | ) catch return error.TlsInitializationFailed, | |
| 341 | 340 | }; |
| 342 | // TODO data race here on ca_bundle if the user sets next_https_rescan_certs to true | |
| 343 | tls.client.init(&tls.reader, &tls.writer, .{ | |
| 344 | .host = .{ .explicit = remote_host }, | |
| 345 | .ca = .{ .bundle = client.ca_bundle }, | |
| 346 | .ssl_key_log = client.ssl_key_log, | |
| 347 | }) catch return error.TlsInitializationFailed; | |
| 348 | // This is appropriate for HTTPS because the HTTP headers contain | |
| 349 | // the content length which is used to detect truncation attacks. | |
| 350 | tls.client.allow_truncation_attacks = true; | |
| 351 | ||
| 352 | 341 | return tls; |
| 353 | 342 | } |
| 354 | 343 | |
| ... | ... | @@ -404,26 +393,52 @@ pub const Connection = struct { |
| 404 | 393 | } |
| 405 | 394 | } |
| 406 | 395 | |
| 396 | /// HTTP protocol from client to server. | |
| 397 | /// This either goes directly to `stream_writer`, or to a TLS client. | |
| 398 | pub fn writer(c: *Connection) *Writer { | |
| 399 | return switch (c.protocol) { | |
| 400 | .tls => { | |
| 401 | if (disable_tls) unreachable; | |
| 402 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 403 | return &tls.client.writer; | |
| 404 | }, | |
| 405 | .plain => &c.stream_writer.interface, | |
| 406 | }; | |
| 407 | } | |
| 408 | ||
| 409 | /// HTTP protocol from server to client. | |
| 410 | /// This either comes directly from `stream_reader`, or from a TLS client. | |
| 411 | pub fn reader(c: *const Connection) *Reader { | |
| 412 | return switch (c.protocol) { | |
| 413 | .tls => { | |
| 414 | if (disable_tls) unreachable; | |
| 415 | const tls: *Tls = @fieldParentPtr("connection", c); | |
| 416 | return &tls.client.reader; | |
| 417 | }, | |
| 418 | .plain => c.stream_reader.interface(), | |
| 419 | }; | |
| 420 | } | |
| 421 | ||
| 407 | 422 | pub fn flush(c: *Connection) Writer.Error!void { |
| 408 | try c.writer.flush(); | |
| 409 | 423 | if (c.protocol == .tls) { |
| 410 | 424 | if (disable_tls) unreachable; |
| 411 | 425 | const tls: *Tls = @fieldParentPtr("connection", c); |
| 412 | try tls.writer.flush(); | |
| 426 | try tls.client.writer.flush(); | |
| 413 | 427 | } |
| 428 | try c.stream_writer.interface.flush(); | |
| 414 | 429 | } |
| 415 | 430 | |
| 416 | 431 | /// If the connection is a TLS connection, sends the close_notify alert. |
| 417 | 432 | /// |
| 418 | 433 | /// Flushes all buffers. |
| 419 | 434 | pub fn end(c: *Connection) Writer.Error!void { |
| 420 | try c.writer.flush(); | |
| 421 | 435 | if (c.protocol == .tls) { |
| 422 | 436 | if (disable_tls) unreachable; |
| 423 | 437 | const tls: *Tls = @fieldParentPtr("connection", c); |
| 424 | 438 | try tls.client.end(); |
| 425 | try tls.writer.flush(); | |
| 439 | try tls.client.writer.flush(); | |
| 426 | 440 | } |
| 441 | try c.stream_writer.interface.flush(); | |
| 427 | 442 | } |
| 428 | 443 | }; |
| 429 | 444 | |
| ... | ... | @@ -660,14 +675,14 @@ pub const Response = struct { |
| 660 | 675 | |
| 661 | 676 | /// If compressed body has been negotiated this will return compressed bytes. |
| 662 | 677 | /// |
| 663 | /// If the returned `std.io.Reader` returns `error.ReadFailed` the error is | |
| 678 | /// If the returned `Reader` returns `error.ReadFailed` the error is | |
| 664 | 679 | /// available via `bodyErr`. |
| 665 | 680 | /// |
| 666 | 681 | /// Asserts that this function is only called once. |
| 667 | 682 | /// |
| 668 | 683 | /// See also: |
| 669 | 684 | /// * `readerDecompressing` |
| 670 | pub fn reader(response: *Response, buffer: []u8) std.io.Reader { | |
| 685 | pub fn reader(response: *Response, buffer: []u8) Reader { | |
| 671 | 686 | const req = response.request; |
| 672 | 687 | if (!req.method.responseHasBody()) return .ending; |
| 673 | 688 | const head = &response.head; |
| ... | ... | @@ -676,7 +691,7 @@ pub const Response = struct { |
| 676 | 691 | |
| 677 | 692 | /// If compressed body has been negotiated this will return decompressed bytes. |
| 678 | 693 | /// |
| 679 | /// If the returned `std.io.Reader` returns `error.ReadFailed` the error is | |
| 694 | /// If the returned `Reader` returns `error.ReadFailed` the error is | |
| 680 | 695 | /// available via `bodyErr`. |
| 681 | 696 | /// |
| 682 | 697 | /// Asserts that this function is only called once. |
| ... | ... | @@ -687,7 +702,7 @@ pub const Response = struct { |
| 687 | 702 | response: *Response, |
| 688 | 703 | decompressor: *http.Decompressor, |
| 689 | 704 | decompression_buffer: []u8, |
| 690 | ) std.io.Reader { | |
| 705 | ) Reader { | |
| 691 | 706 | const head = &response.head; |
| 692 | 707 | return response.request.reader.bodyReaderDecompressing( |
| 693 | 708 | head.transfer_encoding, |
| ... | ... | @@ -698,7 +713,7 @@ pub const Response = struct { |
| 698 | 713 | ); |
| 699 | 714 | } |
| 700 | 715 | |
| 701 | /// After receiving `error.ReadFailed` from the `std.io.Reader` returned by | |
| 716 | /// After receiving `error.ReadFailed` from the `Reader` returned by | |
| 702 | 717 | /// `reader` or `readerDecompressing`, this function accesses the |
| 703 | 718 | /// more specific error code. |
| 704 | 719 | pub fn bodyErr(response: *const Response) ?http.Reader.BodyError { |
| ... | ... | @@ -835,8 +850,8 @@ pub const Request = struct { |
| 835 | 850 | /// |
| 836 | 851 | /// See also: |
| 837 | 852 | /// * `sendBodyUnflushed` |
| 838 | pub fn sendBody(r: *Request) Writer.Error!http.BodyWriter { | |
| 839 | const result = try sendBodyUnflushed(r); | |
| 853 | pub fn sendBody(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | |
| 854 | const result = try sendBodyUnflushed(r, buffer); | |
| 840 | 855 | try r.connection.?.flush(); |
| 841 | 856 | return result; |
| 842 | 857 | } |
| ... | ... | @@ -846,17 +861,44 @@ pub const Request = struct { |
| 846 | 861 | /// |
| 847 | 862 | /// See also: |
| 848 | 863 | /// * `sendBody` |
| 849 | pub fn sendBodyUnflushed(r: *Request) Writer.Error!http.BodyWriter { | |
| 864 | pub fn sendBodyUnflushed(r: *Request, buffer: []u8) Writer.Error!http.BodyWriter { | |
| 850 | 865 | assert(r.method.requestHasBody()); |
| 851 | 866 | try sendHead(r); |
| 852 | return .{ | |
| 853 | .http_protocol_output = &r.connection.?.writer, | |
| 854 | .state = switch (r.transfer_encoding) { | |
| 855 | .chunked => .{ .chunked = .init }, | |
| 856 | .content_length => |len| .{ .content_length = len }, | |
| 857 | .none => .none, | |
| 867 | const http_protocol_output = &r.connection.?.writer; | |
| 868 | return switch (r.transfer_encoding) { | |
| 869 | .chunked => .{ | |
| 870 | .http_protocol_output = http_protocol_output, | |
| 871 | .state = .{ .chunked = .init }, | |
| 872 | .interface = .{ | |
| 873 | .buffer = buffer, | |
| 874 | .interface = &.{ | |
| 875 | .drain = http.BodyWriter.chunkedDrain, | |
| 876 | .sendFile = http.BodyWriter.chunkedSendFile, | |
| 877 | }, | |
| 878 | }, | |
| 879 | }, | |
| 880 | .content_length => |len| .{ | |
| 881 | .http_protocol_output = http_protocol_output, | |
| 882 | .state = .{ .content_length = len }, | |
| 883 | .interface = .{ | |
| 884 | .buffer = buffer, | |
| 885 | .interface = &.{ | |
| 886 | .drain = http.BodyWriter.contentLengthDrain, | |
| 887 | .sendFile = http.BodyWriter.contentLengthSendFile, | |
| 888 | }, | |
| 889 | }, | |
| 890 | }, | |
| 891 | .none => .{ | |
| 892 | .http_protocol_output = http_protocol_output, | |
| 893 | .state = .none, | |
| 894 | .interface = .{ | |
| 895 | .buffer = buffer, | |
| 896 | .interface = &.{ | |
| 897 | .drain = http.BodyWriter.noneDrain, | |
| 898 | .sendFile = http.BodyWriter.noneSendFile, | |
| 899 | }, | |
| 900 | }, | |
| 858 | 901 | }, |
| 859 | .elide = false, | |
| 860 | 902 | }; |
| 861 | 903 | } |
| 862 | 904 |
lib/std/http/Server.zig+33-9| ... | ... | @@ -381,6 +381,8 @@ pub const Request = struct { |
| 381 | 381 | content_length: ?u64 = null, |
| 382 | 382 | /// Options that are shared with the `respond` method. |
| 383 | 383 | respond_options: RespondOptions = .{}, |
| 384 | /// Used by `http.BodyWriter`. | |
| 385 | buffer: []u8, | |
| 384 | 386 | }; |
| 385 | 387 | |
| 386 | 388 | /// The header is not guaranteed to be sent until `BodyWriter.flush` or |
| ... | ... | @@ -436,16 +438,38 @@ pub const Request = struct { |
| 436 | 438 | |
| 437 | 439 | try out.writeAll("\r\n"); |
| 438 | 440 | const elide_body = request.head.method == .HEAD; |
| 439 | ||
| 440 | return .{ | |
| 441 | const state: http.BodyWriter.State = if (o.transfer_encoding) |te| switch (te) { | |
| 442 | .chunked => .{ .chunked = .init }, | |
| 443 | .none => .none, | |
| 444 | } else if (options.content_length) |len| .{ | |
| 445 | .content_length = len, | |
| 446 | } else .{ .chunked = .init }; | |
| 447 | ||
| 448 | return if (elide_body) .{ | |
| 449 | .http_protocol_output = request.server.out, | |
| 450 | .state = state, | |
| 451 | .interface = .discarding(options.buffer), | |
| 452 | } else .{ | |
| 441 | 453 | .http_protocol_output = request.server.out, |
| 442 | .state = if (o.transfer_encoding) |te| switch (te) { | |
| 443 | .chunked => .{ .chunked = .init }, | |
| 444 | .none => .none, | |
| 445 | } else if (options.content_length) |len| .{ | |
| 446 | .content_length = len, | |
| 447 | } else .{ .chunked = .init }, | |
| 448 | .elide = elide_body, | |
| 454 | .state = state, | |
| 455 | .interface = .{ | |
| 456 | .buffer = options.buffer, | |
| 457 | .vtable = switch (state) { | |
| 458 | .none => &.{ | |
| 459 | .drain = http.BodyWriter.noneDrain, | |
| 460 | .sendFile = http.BodyWriter.noneSendFile, | |
| 461 | }, | |
| 462 | .content_length => &.{ | |
| 463 | .drain = http.BodyWriter.contentLengthDrain, | |
| 464 | .sendFile = http.BodyWriter.contentLengthSendFile, | |
| 465 | }, | |
| 466 | .chunked => &.{ | |
| 467 | .drain = http.BodyWriter.chunkedDrain, | |
| 468 | .sendFile = http.BodyWriter.chunkedSendFile, | |
| 469 | }, | |
| 470 | .end => unreachable, | |
| 471 | }, | |
| 472 | }, | |
| 449 | 473 | }; |
| 450 | 474 | } |
| 451 | 475 |
lib/std/io/Reader.zig-9| ... | ... | @@ -13,7 +13,6 @@ const Limit = std.io.Limit; |
| 13 | 13 | |
| 14 | 14 | pub const Limited = @import("Reader/Limited.zig"); |
| 15 | 15 | |
| 16 | context: ?*anyopaque = null, | |
| 17 | 16 | vtable: *const VTable, |
| 18 | 17 | buffer: []u8, |
| 19 | 18 | /// Number of bytes which have been consumed from `buffer`. |
| ... | ... | @@ -88,7 +87,6 @@ pub const ShortError = error{ |
| 88 | 87 | }; |
| 89 | 88 | |
| 90 | 89 | pub const failing: Reader = .{ |
| 91 | .context = undefined, | |
| 92 | 90 | .vtable = &.{ |
| 93 | 91 | .read = failingStream, |
| 94 | 92 | .discard = failingDiscard, |
| ... | ... | @@ -107,7 +105,6 @@ pub fn limited(r: *Reader, limit: Limit, buffer: []u8) Limited { |
| 107 | 105 | /// Constructs a `Reader` such that it will read from `buffer` and then end. |
| 108 | 106 | pub fn fixed(buffer: []const u8) Reader { |
| 109 | 107 | return .{ |
| 110 | .context = undefined, | |
| 111 | 108 | .vtable = &.{ |
| 112 | 109 | .stream = endingStream, |
| 113 | 110 | .discard = endingDiscard, |
| ... | ... | @@ -1402,12 +1399,6 @@ test "readAlloc when the backing reader provides one byte at a time" { |
| 1402 | 1399 | self.curr += 1; |
| 1403 | 1400 | return 1; |
| 1404 | 1401 | } |
| 1405 | ||
| 1406 | fn reader(self: *@This()) std.io.Reader { | |
| 1407 | return .{ | |
| 1408 | .context = self, | |
| 1409 | }; | |
| 1410 | } | |
| 1411 | 1402 | }; |
| 1412 | 1403 | |
| 1413 | 1404 | const str = "This is a test"; |
lib/std/io/Writer.zig-6| ... | ... | @@ -9,12 +9,6 @@ const File = std.fs.File; |
| 9 | 9 | const testing = std.testing; |
| 10 | 10 | const Allocator = std.mem.Allocator; |
| 11 | 11 | |
| 12 | /// There are two strategies for obtaining context; one can use this field, or | |
| 13 | /// embed the `Writer` and use `@fieldParentPtr`. This field must be either set | |
| 14 | /// to a valid pointer or left as `null` because the interface will sometimes | |
| 15 | /// check if this pointer value is a known special value, for example to make | |
| 16 | /// `writableVector` work. | |
| 17 | context: ?*anyopaque = null, | |
| 18 | 12 | vtable: *const VTable, |
| 19 | 13 | /// If this has length zero, the writer is unbuffered, and `flush` is a no-op. |
| 20 | 14 | buffer: []u8, |