// The pure client-side HTTP/2 protocol engine — the transport-independent core of
// a gRPC Channel, symmetric to `H2Server`. It allocates client stream ids, builds
// request HEADERS (HPACK-encoded) and length-prefixed DATA honouring the send
// windows, and turns the response frames back into `(:status, grpc-status, initial
// metadata, reply messages, trailers)`. No sockets and no async: `feed` is a total
// function over frames, so the whole client path runs in-memory on every backend;
// the `Channel` driver in `net/` only pumps bytes.
///|
/// One client-side call: its stream id, the request side (the length-prefixed
/// request body still to send, the send window, and whether the request has been
/// half-closed), and the response side (the accumulating DATA with a cursor over
/// the messages already pulled out, the recv window, the captured `:status` and
/// `grpc-status`, and the response initial and trailing metadata).
pub(all) struct ClientCall {
id : Int
path : Bytes
authority : Bytes
metadata : Array[Header]
timeout : Bytes?
mut out : Bytes
mut out_off : Int
mut req_ended : Bool
mut req_headers_sent : Bool
mut req_fin_sent : Bool
mut send_window : Int
data : Buffer
mut data_off : Int
mut messages : Array[Bytes]
mut recv_window : Int
mut status : Bytes
mut grpc_status : Int?
// The peer's `grpc-message` (percent-decoded); empty when none was sent.
mut grpc_message : String
// The response's `grpc-encoding` (empty = identity). A reply message with the
// compression flag set under `gzip` is inflated before it reaches the caller.
mut resp_encoding : Bytes
resp_headers : Array[Header]
resp_trailers : Array[Header]
mut headers_seen : Bool
// A response header block (initial metadata or trailers) split across HEADERS +
// CONTINUATION is accumulated here and decoded only once END_HEADERS arrives, since
// the HPACK decoder is stateful and must see each complete block exactly once.
header_block : Buffer
mut in_headers : Bool
mut pending_end_stream : Bool
// A reply message could not be decompressed (or arrived under an unsupported
// encoding). The call fails INTERNAL and a later trailer `grpc-status` must not
// overwrite that back to OK.
mut decode_failed : Bool
// Left set when a GOAWAY names a last-processed stream id below this call's: the
// server never saw this stream, so re-issuing the RPC is safe (gRPC transport
// GOAWAY, RFC 7540 §6.8).
mut retryable : Bool
mut done : Bool
}
///|
fn ClientCall::new(
id : Int,
path : Bytes,
authority : Bytes,
metadata : Array[Header],
timeout : Bytes?,
send_window : Int,
) -> ClientCall {
{
id,
path,
authority,
metadata,
timeout,
out: b"",
out_off: 0,
req_ended: false,
req_headers_sent: false,
req_fin_sent: false,
send_window,
data: Buffer(),
data_off: 0,
messages: [],
recv_window: default_window_size,
status: b"",
grpc_status: None,
grpc_message: "",
resp_encoding: b"",
resp_headers: [],
resp_trailers: [],
headers_seen: false,
header_block: Buffer(),
in_headers: false,
pending_end_stream: false,
decode_failed: false,
retryable: false,
done: false,
}
}
///|
/// The completed result of a call: the HTTP `:status`, the numeric `grpc-status`
/// (`-1` if the peer never sent one), the response initial metadata, the reply
/// messages in order, and the trailing metadata.
pub(all) struct CallReply {
status : Bytes
grpc_status : Int
grpc_message : String
headers : Array[Header]
messages : Array[Bytes]
trailers : Array[Header]
}
///|
/// The client side of one HTTP/2 connection: the HPACK codec pair (stateful across
/// every call on the connection), the live calls keyed by stream id, the next
/// odd stream id to allocate (§5.1.1), and the connection-level flow-control state
/// bounded by the peer's SETTINGS.
pub struct H2Client {
encoder : HpackEncoder
decoder : HpackDecoder
calls : Map[Int, ClientCall]
mut next_id : Int
mut conn_send_window : Int
mut conn_recv_window : Int
mut remote_initial_window : Int
mut remote_max_frame : Int
mut goaway_received : Bool
// The peer's last processed stream id from its GOAWAY (RFC 7540 §6.8): calls above
// it were never seen by the server, so they are safe to re-issue on a fresh
// connection. Mirrors `H2Server::last_client_stream`.
mut goaway_last_stream_id : Int
// Header blocks arriving for streams this client no longer tracks. They still
// have to reach the decoder — HPACK's table is per-connection — so they are
// accumulated here and dropped once complete.
orphan_blocks : Map[Int, @buffer.Buffer]
}
///|
/// A request header block as HEADERS plus as many CONTINUATION frames as the peer's
/// `SETTINGS_MAX_FRAME_SIZE` requires. Mirrors the server's `emit_header_frames`;
/// both exist because a field block is one logical unit that the framing layer has
/// to cut up, and a peer treats an oversized frame as a connection error.
fn emit_request_header_frames(
stream_id : Int,
block : Bytes,
end_stream : Bool,
max_frame_in : Int,
) -> Array[Frame] {
let max_frame = if max_frame_in < 1 {
default_max_frame_size
} else {
max_frame_in
}
let frames : Array[Frame] = []
if block.length() <= max_frame {
frames.push(
Headers(
stream_id~,
fragment=block,
end_stream~,
end_headers=true,
priority=None,
padding=0,
),
)
return frames
}
frames.push(
Headers(
stream_id~,
fragment=block[0:max_frame].to_owned(),
end_stream~,
end_headers=false,
priority=None,
padding=0,
),
)
let mut off = max_frame
while off < block.length() {
let end = if off + max_frame < block.length() {
off + max_frame
} else {
block.length()
}
frames.push(
Continuation(
stream_id~,
fragment=block[off:end].to_owned(),
end_headers=end == block.length(),
),
)
off = end
}
frames
}
///|
/// A fresh client engine with no live calls. The first call takes stream id 1.
pub fn H2Client::new() -> H2Client {
{
encoder: HpackEncoder::new(),
decoder: HpackDecoder::new(),
calls: Map([]),
next_id: 1,
conn_send_window: default_window_size,
conn_recv_window: default_window_size,
remote_initial_window: default_window_size,
remote_max_frame: default_max_frame_size,
goaway_received: false,
goaway_last_stream_id: 0,
orphan_blocks: Map([]),
}
}
///|
/// The client's opening frames (RFC 7540 §3.5): a SETTINGS frame disabling server
/// push. Written right after the 24-octet connection preface bytes, before any
/// request.
pub fn H2Client::preface(self : H2Client) -> Array[Frame] {
ignore(self)
[Settings(params=[(settings_enable_push, 0)], ack=false)]
}
///|
/// Open a new call on this connection for `path` (`/pkg.Service/Method`), returning
/// its freshly allocated stream id. `metadata` is sent as custom request HEADERS;
/// `timeout_millis`, when set, becomes the `grpc-timeout` header. The request body
/// is added with `send` and half-closed with `close_send`.
pub fn H2Client::open(
self : H2Client,
path : String,
metadata? : Array[Header] = [],
authority? : String = "127.0.0.1",
timeout_millis? : Int? = None,
) -> Int {
// A draining connection (the peer sent GOAWAY) starts no new stream: it would race
// the shutdown and never be processed. Refuse with the reserved stream id 0 (§5.1.1,
// never a real call) so no HEADERS leave and no id is consumed; the caller surfaces
// this as UNAVAILABLE (the client's unusable-transport status, `unavailable_reply`).
if self.goaway_received {
return 0
}
let id = self.next_id
self.next_id = self.next_id + 2
let timeout = match timeout_millis {
Some(ms) => Some(encode_grpc_timeout(ms))
None => None
}
let call = ClientCall::new(
id,
ascii_to_bytes(path),
ascii_to_bytes(authority),
metadata,
timeout,
self.remote_initial_window,
)
self.calls[id] = call
id
}
///|
/// Append one request `message` to a call and return the frames to write now (the
/// request HEADERS the first time, then as much length-prefixed DATA as the send
/// windows allow). Set `end` on the last message to half-close the request.
pub fn H2Client::send(
self : H2Client,
id : Int,
message : Bytes,
end? : Bool = false,
) -> Array[Frame] {
match self.calls.get(id) {
Some(c) => {
c.out = cat(c.out, encode_message(message))
if end {
c.req_ended = true
}
self.produce_request(c)
}
None => []
}
}
///|
/// Half-close the request side of a call (no more request messages) and return any
/// frames that completes — the trailing END_STREAM.
pub fn H2Client::close_send(self : H2Client, id : Int) -> Array[Frame] {
match self.calls.get(id) {
Some(c) => {
c.req_ended = true
self.produce_request(c)
}
None => []
}
}
///|
/// Open a unary call and return `(stream_id, frames_to_write)` in one step: the
/// request HEADERS and the single length-prefixed request message with END_STREAM.
pub fn H2Client::unary(
self : H2Client,
path : String,
request : Bytes,
metadata? : Array[Header] = [],
authority? : String = "127.0.0.1",
timeout_millis? : Int? = None,
) -> (Int, Array[Frame]) {
let id = self.open(path, metadata~, authority~, timeout_millis~)
(id, self.send(id, request, end=true))
}
///|
/// Emit the request frames a call can send right now: the HEADERS once, then as
/// much buffered DATA as the connection and stream send windows and the peer's
/// max-frame size allow, the last frame carrying END_STREAM once the request is
/// half-closed. A half-close with an already-drained body emits an empty
/// END_STREAM DATA.
fn H2Client::produce_request(self : H2Client, c : ClientCall) -> Array[Frame] {
let frames : Array[Frame] = []
if !c.req_headers_sent {
let headers : Array[Header] = [
{ name: b":method", value: b"POST", },
{ name: b":scheme", value: b"http", },
{ name: b":path", value: c.path, },
{ name: b":authority", value: c.authority, },
{ name: b"te", value: b"trailers", },
{ name: b"content-type", value: b"application/grpc", },
]
match c.timeout {
Some(t) => headers.push({ name: b"grpc-timeout", value: t, })
None => ()
}
for h in c.metadata {
// A `-bin` metadata value goes on the wire base64-encoded.
headers.push({
name: h.name,
value: metadata_value_to_wire(h.name, h.value),
})
}
let block = self.encoder.encode(headers)
let end = c.req_ended && c.out.length() == 0
// A request carrying more metadata than the peer's MAX_FRAME_SIZE has to go out
// as HEADERS + CONTINUATION, or the peer answers FRAME_SIZE_ERROR.
for f in emit_request_header_frames(c.id, block, end, self.remote_max_frame) {
frames.push(f)
}
c.req_headers_sent = true
if end {
c.req_fin_sent = true
}
}
while c.out_off < c.out.length() {
let remaining = c.out.length() - c.out_off
let budget = min3(
self.conn_send_window,
c.send_window,
self.remote_max_frame,
)
if budget <= 0 {
break
}
let n = if remaining < budget { remaining } else { budget }
let chunk = c.out[c.out_off:c.out_off + n].to_owned()
c.out_off = c.out_off + n
self.conn_send_window = self.conn_send_window - n
c.send_window = c.send_window - n
let last = c.out_off >= c.out.length() && c.req_ended
if last {
c.req_fin_sent = true
}
frames.push(Data(stream_id=c.id, data=chunk, end_stream=last, padding=0))
}
if c.req_ended && !c.req_fin_sent && c.out_off >= c.out.length() {
c.req_fin_sent = true
frames.push(Data(stream_id=c.id, data=b"", end_stream=true, padding=0))
}
frames
}
///|
/// Feed one decoded response frame to the engine, advancing all state and returning
/// the frames to write back (SETTINGS ack, PING pong, WINDOW_UPDATE replenishing a
/// receive window, and — once a WINDOW_UPDATE lifts back-pressure — any remaining
/// request DATA). Captures `:status`, `grpc-status`, response metadata, and the
/// reassembled reply messages.
pub fn H2Client::feed(self : H2Client, frame : Frame) -> Array[Frame] raise {
match frame {
Settings(params~, ack~) =>
if ack {
[]
} else {
for p in params {
self.apply_setting(p.0, p.1)
}
[Settings(params=[], ack=true)]
}
Ping(payload~, ack~) => if ack { [] } else { [Ping(payload~, ack=true)] }
WindowUpdate(stream_id~, increment~) => {
if stream_id == 0 {
self.conn_send_window = add_window(self.conn_send_window, increment)
} else {
match self.calls.get(stream_id) {
Some(c) => c.send_window = add_window(c.send_window, increment)
None => ()
}
}
self.pump_requests()
}
Headers(stream_id~, fragment~, end_stream~, end_headers~, ..) => {
match self.calls.get(stream_id) {
Some(c) => {
// Start a new header block; END_STREAM rides the HEADERS frame, so remember
// it to apply once the block (which may continue across CONTINUATION) ends.
c.header_block.write_bytes(fragment)
guard_header_size(c.header_block)
c.pending_end_stream = end_stream
if end_headers {
self.deliver_headers(c)
} else {
c.in_headers = true
}
}
// A block for a stream we no longer track — trailers racing a release, or a
// stream we reset — still has to go through the decoder. HPACK state is
// per-connection: skipping one block desynchronises the table and every
// later call on this connection decodes garbage.
None => self.drain_orphan_block(stream_id, fragment, end_headers)
}
[]
}
Continuation(stream_id~, fragment~, end_headers~) =>
match self.calls.get(stream_id) {
Some(c) => {
if c.in_headers {
c.header_block.write_bytes(fragment)
guard_header_size(c.header_block)
if end_headers {
self.deliver_headers(c)
}
}
[]
}
None => {
self.drain_orphan_block(stream_id, fragment, end_headers)
[]
}
}
Data(stream_id~, data~, end_stream~, padding~) =>
match self.calls.get(stream_id) {
Some(c) => {
let flow = data.length() + padding + (if padding > 0 { 1 } else { 0 })
self.conn_recv_window = self.conn_recv_window - flow
c.recv_window = c.recv_window - flow
c.data.write_bytes(data)
drain_call_messages(c)
if end_stream {
c.done = true
}
self.replenish(c)
}
None => []
}
RstStream(stream_id~, ..) => {
match self.calls.get(stream_id) {
Some(c) => c.done = true
None => ()
}
[]
}
GoAway(last_stream_id~, ..) => {
// The peer is draining (RFC 7540 §6.8): remember it so `open` refuses new
// streams, and mark every in-flight call above its last processed id retryable —
// those were never seen by the server, so re-issuing them is safe (gRPC A6).
self.goaway_received = true
self.goaway_last_stream_id = last_stream_id
for _, c in self.calls {
if c.id > last_stream_id {
c.retryable = true
}
}
[]
}
Priority(..) | PushPromise(..) | Unknown(..) => []
}
}
///|
/// Accumulate a header block for a stream this client no longer tracks, and decode
/// it once complete. The result is thrown away — nobody is waiting for it — but the
/// decode itself is mandatory: HPACK's table is connection-wide, so a block that is
/// never fed leaves our decoder disagreeing with the peer's encoder for good.
fn H2Client::drain_orphan_block(
self : H2Client,
stream_id : Int,
fragment : Bytes,
end_headers : Bool,
) -> Unit raise {
let buf = match self.orphan_blocks.get(stream_id) {
Some(b) => b
None => {
let b = @buffer.Buffer()
self.orphan_blocks[stream_id] = b
b
}
}
buf.write_bytes(fragment)
guard_header_size(buf)
if end_headers {
let _ = self.decoder.decode(buf.to_bytes())
self.orphan_blocks.remove(stream_id)
}
}
///|
/// Decode this call's now-complete response header block (HPACK is stateful, so a
/// block spanning HEADERS + CONTINUATION is decoded exactly once, here). The first
/// completed block is initial metadata; a later one is trailers. Applies the
/// END_STREAM the initiating HEADERS carried.
fn H2Client::deliver_headers(self : H2Client, c : ClientCall) -> Unit raise {
let headers = self.decoder.decode(c.header_block.to_bytes())
for h in headers {
if h.name == b":status" {
c.status = h.value
} else if h.name == b"grpc-status" {
c.grpc_status = Some(ascii_bytes_to_int(h.value))
} else if h.name == b"grpc-message" {
c.grpc_message = percent_decode(h.value)
} else if h.name == b"grpc-encoding" {
c.resp_encoding = h.value
} else if !c.headers_seen {
// A `-bin` metadata value arrives base64-encoded; surface raw bytes.
if !is_reserved_header(h.name) {
c.resp_headers.push({
name: h.name,
value: metadata_value_from_wire(h.name, h.value),
})
}
} else {
c.resp_trailers.push({
name: h.name,
value: metadata_value_from_wire(h.name, h.value),
})
}
}
c.headers_seen = true
c.header_block.reset()
c.in_headers = false
if c.pending_end_stream {
c.done = true
}
}
///|
/// Apply a peer SETTINGS parameter, mirroring the delta of a changed
/// `INITIAL_WINDOW_SIZE` onto every open call's send window (RFC 7540 §6.9.2).
fn H2Client::apply_setting(self : H2Client, id : Int, value : Int) -> Unit {
if id == settings_initial_window_size {
// Ignore an out-of-range window (RFC 7540 §6.5.2 caps it at 2^31-1) rather than
// letting a negative value corrupt every call's send-window math.
if value < 0 {
return
}
let delta = value - self.remote_initial_window
self.remote_initial_window = value
for _, c in self.calls {
c.send_window = add_window(c.send_window, delta)
}
} else if id == settings_max_frame_size {
// Clamp to the RFC 7540 §6.5.2 range [2^14, 2^24-1].
self.remote_max_frame = if value < default_max_frame_size {
default_max_frame_size
} else if value > 0xFFFFFF {
0xFFFFFF
} else {
value
}
} else if id == settings_header_table_size {
if value >= 0 {
self.encoder.table.set_max_size(value)
}
}
}
///|
/// Emit connection- and stream-level WINDOW_UPDATE frames when a receive window has
/// fallen below half the default, replenishing it to the default (RFC 7540 §6.9).
fn H2Client::replenish(self : H2Client, c : ClientCall) -> Array[Frame] {
let frames : Array[Frame] = []
let threshold = default_window_size / 2
if self.conn_recv_window < threshold {
let inc = default_window_size - self.conn_recv_window
self.conn_recv_window = self.conn_recv_window + inc
frames.push(WindowUpdate(stream_id=0, increment=inc))
}
if c.recv_window < threshold {
let inc = default_window_size - c.recv_window
c.recv_window = c.recv_window + inc
frames.push(WindowUpdate(stream_id=c.id, increment=inc))
}
frames
}
///|
/// Continue any call whose request body is not fully sent after a WINDOW_UPDATE
/// lifted back-pressure.
fn H2Client::pump_requests(self : H2Client) -> Array[Frame] {
let frames : Array[Frame] = []
for _, c in self.calls {
if c.req_headers_sent && !c.req_fin_sent {
for f in self.produce_request(c) {
frames.push(f)
}
}
}
frames
}
///|
/// Drop everything the engine still holds for a finished call: its buffered body,
/// messages, header block and trailers. The map is only ever inserted into otherwise,
/// so a long-lived connection would keep one full request and response per RPC it has
/// ever made, and every WINDOW_UPDATE and SETTINGS frame would walk that whole history.
/// An unknown or already-released id is a no-op.
pub fn H2Client::release(self : H2Client, id : Int) -> Unit {
self.calls.remove(id)
}
///|
/// Whether a call has fully completed (its response ended). An unknown id counts as
/// done so a driver loop terminates.
pub fn H2Client::is_done(self : H2Client, id : Int) -> Bool {
match self.calls.get(id) {
Some(c) => c.done
None => true
}
}
///|
/// Whether the peer has sent GOAWAY; a draining connection opens no new stream, so
/// the driver picks a fresh one for further calls. Mirrors `H2Server::goaway_received`.
pub fn H2Client::goaway_received(self : H2Client) -> Bool {
self.goaway_received
}
///|
/// The peer's last processed stream id from its GOAWAY (`0` if none was seen); a call
/// whose stream id is above it was never handled by the server.
pub fn H2Client::goaway_last_stream_id(self : H2Client) -> Int {
self.goaway_last_stream_id
}
///|
/// Whether call `id` was left retryable by a GOAWAY: its stream id is above the peer's
/// last processed id, so the server never saw it and re-issuing the RPC on a fresh
/// connection is safe (gRPC transport GOAWAY / RFC 7540 §6.8). False for a call at or
/// below that id — it may have been processed — and when no GOAWAY arrived.
pub fn H2Client::is_retryable(self : H2Client, id : Int) -> Bool {
match self.calls.get(id) {
Some(c) => c.retryable
None => false
}
}
///|
/// The completed result of a call. Meaningful once `is_done` is true.
pub fn H2Client::reply(self : H2Client, id : Int) -> CallReply {
match self.calls.get(id) {
Some(c) => {
let grpc = if c.decode_failed {
Status::code(Internal)
} else {
match c.grpc_status {
Some(v) => v
None => -1
}
}
{
status: c.status,
grpc_status: grpc,
grpc_message: c.grpc_message,
headers: c.resp_headers,
messages: c.messages,
trailers: c.resp_trailers,
}
}
None =>
{
status: b"",
grpc_status: -1,
grpc_message: "",
headers: [],
messages: [],
trailers: [],
}
}
}
///|
/// Whether a call has reply messages buffered but not yet taken — for reading a streaming
/// response incrementally as it arrives, rather than all at once via `reply`.
pub fn H2Client::has_messages(self : H2Client, id : Int) -> Bool {
match self.calls.get(id) {
Some(c) => c.messages.length() > 0
None => false
}
}
///|
/// Take every reply message received so far, clearing the call's buffer — the incremental
/// counterpart to `reply`, for a server- or bidi-streaming response read message by message.
pub fn H2Client::take_messages(self : H2Client, id : Int) -> Array[Bytes] {
match self.calls.get(id) {
Some(c) => {
let msgs = c.messages
c.messages = []
msgs
}
None => []
}
}
///|
/// Pull every complete length-prefixed reply message now buffered on a call into
/// `messages`, advancing the read cursor; partial trailing bytes stay buffered.
fn drain_call_messages(c : ClientCall) -> Unit {
let all = c.data.to_bytes()
let n = all.length()
let mut off = c.data_off
while n - off >= 5 {
let compressed = all[off].to_int() != 0
let len = (all[off + 1].to_int() << 24) |
(all[off + 2].to_int() << 16) |
(all[off + 3].to_int() << 8) |
all[off + 4].to_int()
// A high-bit-set prefix decodes negative; reject that or a length past the cap
// (surfacing RESOURCE_EXHAUSTED) instead of slicing the buffer out of bounds.
if len < 0 || len > max_message_size {
if c.grpc_status is None {
c.grpc_status = Some(Status::code(ResourceExhausted))
}
break
}
if n - off < 5 + len {
break
}
let body = all[off + 5:off + 5 + len].to_owned()
// A reply message with the compression flag set is inflated under gzip. A decode
// failure, or a compression encoding the client never advertised, fails the RPC
// with INTERNAL rather than handing the caller the raw compressed bytes as if they
// were the message (gRPC's answer for an undecodable reply).
if compressed {
if c.resp_encoding == b"gzip" {
match (Some(gunzip(body)) catch { _ => None }) {
Some(m) => c.messages.push(m)
None => c.decode_failed = true
}
} else {
c.decode_failed = true
}
} else {
c.messages.push(body)
}
off = off + 5 + len
}
c.data_off = off
}
///|
/// Parse ASCII decimal `Bytes` (a `grpc-status` value) to an `Int`; non-digits stop
/// the scan.
fn ascii_bytes_to_int(b : Bytes) -> Int {
let mut n = 0
for i = 0; i < b.length(); i = i + 1 {
let c = b[i].to_int()
if c < 0x30 || c > 0x39 {
break
}
n = n * 10 + (c - 0x30)
}
n
}
///|
/// Encode whole `millis` as a `grpc-timeout` header value (RFC gRPC HTTP/2 mapping):
/// the `m` (millisecond) unit when the count fits the 8-digit field, else seconds
/// with the `S` unit.
pub fn encode_grpc_timeout(millis : Int) -> Bytes {
if millis <= 99999999 {
cat(int_to_ascii_bytes(millis), b"m")
} else {
cat(int_to_ascii_bytes(millis / 1000), b"S")
}
}
///|
/// The hex-digit value of an ASCII byte (`0-9`, `A-F`, `a-f`), or `None`.
fn hex_val(b : Byte) -> Int? {
let c = b.to_int()
if c >= 0x30 && c <= 0x39 {
Some(c - 0x30)
} else if c >= 0x41 && c <= 0x46 {
Some(c - 0x41 + 10)
} else if c >= 0x61 && c <= 0x66 {
Some(c - 0x61 + 10)
} else {
None
}
}
///|
/// Percent-decode a received `grpc-message` value — the inverse of the server's `percent_encode`
/// (gRPC spec §"Responses"): each `%XX` escape becomes the byte its two hex digits name, every other
/// byte passes through, and a malformed escape is left literal; the result is read as UTF-8.
pub fn percent_decode(v : Bytes) -> String {
let out = Buffer()
let mut i = 0
while i < v.length() {
if v[i] == b'%' && i + 2 < v.length() {
match (hex_val(v[i + 1]), hex_val(v[i + 2])) {
(Some(hi), Some(lo)) => {
out.write_byte((hi * 16 + lo).to_byte())
i = i + 3
}
_ => {
out.write_byte(v[i])
i = i + 1
}
}
} else {
out.write_byte(v[i])
i = i + 1
}
}
@utf8.decode_lossy(out.to_bytes()[:])
}