use errors; use bufio; use bytes; use io; use os; use strconv; use strings; use types; // Configures the Transport-Encoding behavior. // // If set to NONE, no transport decoding or encoding is performed on the message // body, irrespective of the value of the Transport-Encoding header. The user // must perform any required encoding or decoding themselves in this mode. If // set to AUTO, the implementation will examine the Transport-Encoding header // and encode the message body appropriately. // // Most users will want this to be set to auto. export type transport_mode = enum { AUTO = 0, NONE, }; // Configures the Content-Encoding behavior. // // If set to NONE, no transport decoding or encoding is performed on the message // body, irrespective of the value of the Content-Encoding header. The user must // perform any required encoding or decoding themselves in this mode. If set to // AUTO, the implementation will examine the Content-Encoding header and encode // the message body appropriately. // // Most users will want this to be set to AUTO. export type content_mode = enum { AUTO = 0, NONE, }; // Describes an HTTP [[client]]'s transport configuration for a given request. // // The default value of this type sets all parameters to "auto". export type transport = struct { // Desired Transport-Encoding configuration, see [[transport_mode]] for // details. request_transport: transport_mode, response_transport: transport_mode, // Desired Content-Encoding configuration, see [[content_mode]] for // details. request_content: content_mode, response_content: content_mode, }; fn new_reader( conn: io::file, resp: *response, scan: *bufio::scanner, ) (*io::stream | errors::unsupported | protoerr) = { // TODO: Content-Encoding support const cl = header_get(&resp.header, "Content-Length"); const te = header_get(&resp.header, "Transfer-Encoding"); if (cl != "" || te == "") { let length = types::SIZE_MAX; if (cl != "") { length = match (strconv::stoz(cl)) { case let z: size => yield z; case => return protoerr; }; }; return new_identity_reader(conn, scan, length); }; // TODO: Figure out the semantics for closing the stream // The caller should probably be required to close it // It should close/free any intermediate transport/content decoders // And it should not close the actual connection if it's still in the // connection pool // Unless it isn't in the pool, then it should! let stream: io::handle = conn; let buffer: []u8 = bufio::scan_buffer(scan); const iter = strings::tokenize(te, ","); for (const tok => strings::next_token(&iter)) { const te = strings::trim(tok); // XXX: We could add lzw support if someone added it to // hare-compress const next = switch (te) { case "chunked" => yield new_chunked_reader(stream, buffer); case "deflate" => abort(); // TODO case "gzip" => abort(); // TODO case => return errors::unsupported; }; stream = next; buffer = []; }; if (!(stream is *io::stream)) { // Empty Transfer-Encoding header return protoerr; }; return stream as *io::stream; }; type identity_reader = struct { vtable: io::stream, conn: io::file, scan: *bufio::scanner, src: io::limitstream, }; const identity_reader_vtable = io::vtable { reader = &identity_read, closer = &identity_close, ... }; // Creates a new reader that reads data until the response's Content-Length is // reached; i.e. the null Transport-Encoding. fn new_identity_reader( conn: io::file, scan: *bufio::scanner, content_length: size, ) *io::stream = { const scan = alloc(*scan); return alloc(identity_reader { vtable = &identity_reader_vtable, conn = conn, scan = scan, src = io::limitreader(scan, content_length), ... }); }; fn identity_read( s: *io::stream, buf: []u8, ) (size | io::EOF | io::error) = { let rd = s: *identity_reader; assert(rd.vtable == &identity_reader_vtable); return io::read(&rd.src, buf)?; }; fn identity_close(s: *io::stream) (void | io::error) = { let rd = s: *identity_reader; assert(rd.vtable == &identity_reader_vtable); // Flush the remainder of the response in case the caller did not read // it out entirely io::copy(io::empty, &rd.src)?; bufio::finish(rd.scan); free(rd.scan); io::close(rd.conn)?; }; type chunk_state = enum { HEADER, DATA, FOOTER, }; type chunked_reader = struct { vtable: io::stream, conn: io::handle, buffer: [os::BUFSZ]u8, state: chunk_state, // Amount of read-ahead data in buffer pending: size, // Length of current chunk length: size, }; fn new_chunked_reader( conn: io::handle, buffer: []u8, ) *io::stream = { let rd = alloc(chunked_reader { vtable = &chunked_reader_vtable, conn = conn, ... }); rd.buffer[..len(buffer)] = buffer[..]; rd.pending = len(buffer); return rd; }; const chunked_reader_vtable = io::vtable { reader = &chunked_read, ... }; fn chunked_read( s: *io::stream, buf: []u8, ) (size | io::EOF | io::error) = { // XXX: I am not satisfied with this code let rd = s: *chunked_reader; assert(rd.vtable == &chunked_reader_vtable); for (true) switch (rd.state) { case chunk_state::HEADER => let crlf = 0z; for (true) { const n = rd.pending; match (bytes::index(rd.buffer[..n], ['\r', '\n'])) { case let z: size => crlf = z; break; case void => yield; }; if (rd.pending >= len(rd.buffer)) { // Chunk header exceeds buffer size return errors::overflow; }; match (io::read(rd.conn, rd.buffer[rd.pending..])?) { case let n: size => rd.pending += n; case io::EOF => if (rd.pending > 0) { return errors::invalid; }; return io::EOF; }; }; // XXX: Should we do anything with chunk-ext? const header = rd.buffer[..crlf]; const (ln, _) = bytes::cut(header, ';'); const ln = match (strings::fromutf8(ln)) { case let s: str => yield s; case => return errors::invalid; }; match (strconv::stoz(ln, strconv::base::HEX)) { case let z: size => rd.length = z; case => return errors::invalid; }; if (rd.length == 0) { return io::EOF; }; const n = crlf + 2; rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending]; rd.pending -= n; rd.state = chunk_state::DATA; case chunk_state::DATA => if (rd.pending == 0) { match (io::read(rd.conn, rd.buffer)?) { case let n: size => rd.pending += n; case io::EOF => return io::EOF; }; }; let n = len(buf); if (n > rd.pending) { n = rd.pending; }; if (n > rd.length) { n = rd.length; }; buf[..n] = rd.buffer[..n]; rd.buffer[..rd.pending - n] = rd.buffer[n..rd.pending]; rd.pending -= n; rd.length -= n; rd.state = chunk_state::FOOTER; return n; case chunk_state::FOOTER => for (rd.pending < 2) { match (io::read(rd.conn, rd.buffer[rd.pending..])?) { case let n: size => rd.pending += n; case io::EOF => return io::EOF; }; }; if (!bytes::equal(rd.buffer[..2], ['\r', '\n'])) { return errors::invalid; }; rd.buffer[..rd.pending - 2] = rd.buffer[2..rd.pending]; rd.pending -= 2; rd.state = chunk_state::HEADER; }; };