Skip to main content

simoom_h2/
upgrade.rs

1//! h2c upgrade from HTTP/1.1 (RFC 7540 ยง3.2; deprecated by RFC 9113 but still used): the
2//! `HTTP2-Settings` header value is a base64url SETTINGS payload without padding.
3
4const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5
6/// Encodes bytes as unpadded base64url.
7pub fn base64url_encode(b: &[u8]) -> String {
8    let mut s = String::with_capacity(b.len().div_ceil(3) * 4);
9    for c in b.chunks(3) {
10        let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
11        for i in 0..=c.len() {
12            s.push(ALPHABET[(n >> (18 - 6 * i) & 63) as usize] as char);
13        }
14    }
15    s
16}
17
18/// Decodes base64url, with or without padding. Whitespace around the value is ignored; any
19/// other byte outside the alphabet is an error.
20pub fn base64url_decode(s: &[u8]) -> Option<Vec<u8>> {
21    let s = s.trim_ascii();
22    let s = match s.iter().position(|&c| c == b'=') {
23        Some(p) if s[p..].iter().all(|&c| c == b'=') && s[p..].len() <= 2 => &s[..p],
24        Some(_) => return None,
25        None => s,
26    };
27    if s.len() % 4 == 1 {
28        return None;
29    }
30    let mut out = Vec::with_capacity(s.len() * 3 / 4);
31    let mut acc = 0u32;
32    let mut bits = 0;
33    for &c in s {
34        let v = match c {
35            b'A'..=b'Z' => c - b'A',
36            b'a'..=b'z' => c - b'a' + 26,
37            b'0'..=b'9' => c - b'0' + 52,
38            b'-' | b'+' => 62,
39            b'_' | b'/' => 63,
40            _ => return None,
41        };
42        acc = acc << 6 | v as u32;
43        bits += 6;
44        if bits >= 8 {
45            bits -= 8;
46            out.push((acc >> bits) as u8);
47        }
48    }
49    Some(out)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn round_trip() {
58        for n in 0..40usize {
59            let b: Vec<u8> = (0..n).map(|i| (i * 89 + 7) as u8).collect();
60            let e = base64url_encode(&b);
61            assert!(!e.contains('=') && !e.contains('+') && !e.contains('/'));
62            assert_eq!(base64url_decode(e.as_bytes()).unwrap(), b);
63        }
64        assert_eq!(base64url_encode(b"\xfb\xff"), "-_8");
65        assert_eq!(base64url_decode(b"AAMAAABkAAQAAP__").unwrap(), [0, 3, 0, 0, 0, 100, 0, 4, 0, 0, 255, 255]);
66        assert_eq!(base64url_decode(b" QQ== ").unwrap(), b"A");
67        assert!(base64url_decode(b"Q").is_none());
68        assert!(base64url_decode(b"Q=Q=").is_none());
69        assert!(base64url_decode(b"QQ!!").is_none());
70    }
71}