Skip to main content

sim_codec_binary_base64/
base64.rs

1//! Standard base64 encode/decode for the text framing layer.
2//!
3//! A small, dependency-free base64 codec (standard `+/` alphabet with padding)
4//! used to wrap and unwrap binary frames as ASCII text.
5
6use sim_codec::{DecodeBudget, DecodeLimits};
7use sim_kernel::{CodecId, Error, Result};
8
9const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
10
11/// Encodes `bytes` as standard base64 text (the `+/` alphabet, padded).
12///
13/// Shared with `sim-codec-bitwise-base64` so the two text wrappers use one
14/// base64 implementation rather than forking the alphabet and logic.
15pub fn encode_base64(bytes: &[u8]) -> String {
16    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
17    for chunk in bytes.chunks(3) {
18        let b0 = chunk[0];
19        let b1 = *chunk.get(1).unwrap_or(&0);
20        let b2 = *chunk.get(2).unwrap_or(&0);
21        out.push(ALPHABET[(b0 >> 2) as usize] as char);
22        out.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
23        if chunk.len() > 1 {
24            out.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
25        } else {
26            out.push('=');
27        }
28        if chunk.len() > 2 {
29            out.push(ALPHABET[(b2 & 0x3f) as usize] as char);
30        } else {
31            out.push('=');
32        }
33    }
34    out
35}
36
37/// Decodes standard base64 `text` back to bytes under the default decode
38/// limits, failing closed on any invalid character, length, or padding.
39///
40/// Convenience wrapper over [`decode_base64_with_limits`] using
41/// [`DecodeLimits::default`]; prefer the limits-bearing form on the untrusted
42/// decode path so the configured input ceiling is honored.
43///
44/// Shared with `sim-codec-bitwise-base64` (see [`encode_base64`]).
45pub fn decode_base64(codec: CodecId, text: &str) -> Result<Vec<u8>> {
46    decode_base64_with_limits(codec, text, DecodeLimits::default())
47}
48
49/// Decodes standard base64 `text` back to bytes, enforcing `limits` on the raw,
50/// whitespace-stripped, and decoded lengths BEFORE any bulk allocation.
51///
52/// The raw input length is checked against [`DecodeLimits::max_input_bytes`]
53/// first, so a gigabyte of base64 (or whitespace-padded base64) is rejected
54/// before `strip_ascii_whitespace` reserves a buffer for it; the cleaned length
55/// and the projected decoded length are checked in turn. Otherwise identical to
56/// [`decode_base64`]: fails closed on any invalid character, length, or padding.
57///
58/// Shared with `sim-codec-bitwise-base64` (see [`encode_base64`]).
59pub fn decode_base64_with_limits(
60    codec: CodecId,
61    text: &str,
62    limits: DecodeLimits,
63) -> Result<Vec<u8>> {
64    let budget = DecodeBudget::new(limits);
65    budget.check_input_bytes(codec, text.len())?;
66    let clean = strip_ascii_whitespace(codec, text)?;
67    budget.check_input_bytes(codec, clean.len())?;
68    if !clean.len().is_multiple_of(4) {
69        return codec_err(codec, "invalid base64 length");
70    }
71    budget.check_input_bytes(codec, clean.len() / 4 * 3)?;
72    decode_clean_base64(codec, &clean)
73}
74
75fn decode_clean_base64(codec: CodecId, clean: &[u8]) -> Result<Vec<u8>> {
76    let mut out = Vec::with_capacity(clean.len() / 4 * 3);
77    let chunk_count = clean.len() / 4;
78    for (index, chunk) in clean.chunks_exact(4).enumerate() {
79        if chunk[0] == b'=' || chunk[1] == b'=' {
80            return codec_err(codec, "invalid base64 padding");
81        }
82
83        let is_last = index + 1 == chunk_count;
84        let c0 = decode_b64(codec, chunk[0])?;
85        let c1 = decode_b64(codec, chunk[1])?;
86        let c2 = decode_optional_b64(codec, chunk[2])?;
87        let c3 = decode_optional_b64(codec, chunk[3])?;
88
89        match (c2, c3) {
90            (Some(c2), Some(c3)) => {
91                out.push((c0 << 2) | (c1 >> 4));
92                out.push(((c1 & 0x0f) << 4) | (c2 >> 2));
93                out.push(((c2 & 0x03) << 6) | c3);
94            }
95            (Some(c2), None) => {
96                if !is_last {
97                    return codec_err(codec, "base64 padding before final quartet");
98                }
99                if c2 & 0x03 != 0 {
100                    return codec_err(codec, "non-zero base64 padding bits");
101                }
102                out.push((c0 << 2) | (c1 >> 4));
103                out.push(((c1 & 0x0f) << 4) | (c2 >> 2));
104            }
105            (None, None) => {
106                if !is_last {
107                    return codec_err(codec, "base64 padding before final quartet");
108                }
109                if c1 & 0x0f != 0 {
110                    return codec_err(codec, "non-zero base64 padding bits");
111                }
112                out.push((c0 << 2) | (c1 >> 4));
113            }
114            (None, Some(_)) => return codec_err(codec, "invalid base64 padding"),
115        }
116    }
117    Ok(out)
118}
119
120fn strip_ascii_whitespace(codec: CodecId, text: &str) -> Result<Vec<u8>> {
121    let mut clean = Vec::with_capacity(text.len());
122    for byte in text.bytes() {
123        match byte {
124            b' ' | b'\n' | b'\r' | b'\t' => {}
125            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' => clean.push(byte),
126            _ => {
127                return Err(Error::CodecError {
128                    codec,
129                    message: format!("invalid base64 byte 0x{byte:02x}"),
130                });
131            }
132        }
133    }
134    Ok(clean)
135}
136
137fn decode_optional_b64(codec: CodecId, byte: u8) -> Result<Option<u8>> {
138    if byte == b'=' {
139        Ok(None)
140    } else {
141        decode_b64(codec, byte).map(Some)
142    }
143}
144
145fn decode_b64(codec: CodecId, byte: u8) -> Result<u8> {
146    match byte {
147        b'A'..=b'Z' => Ok(byte - b'A'),
148        b'a'..=b'z' => Ok(byte - b'a' + 26),
149        b'0'..=b'9' => Ok(byte - b'0' + 52),
150        b'+' => Ok(62),
151        b'/' => Ok(63),
152        _ => Err(Error::CodecError {
153            codec,
154            message: format!("invalid base64 byte 0x{byte:02x}"),
155        }),
156    }
157}
158
159fn codec_err<T>(codec: CodecId, message: impl Into<String>) -> Result<T> {
160    Err(Error::CodecError {
161        codec,
162        message: message.into(),
163    })
164}