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_kernel::{CodecId, Error, Result};
7
8const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
9
10/// Encodes `bytes` as standard base64 text (the `+/` alphabet, padded).
11///
12/// Shared with `sim-codec-bitwise-base64` so the two text wrappers use one
13/// base64 implementation rather than forking the alphabet and logic.
14pub fn encode_base64(bytes: &[u8]) -> String {
15    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
16    for chunk in bytes.chunks(3) {
17        let b0 = chunk[0];
18        let b1 = *chunk.get(1).unwrap_or(&0);
19        let b2 = *chunk.get(2).unwrap_or(&0);
20        out.push(ALPHABET[(b0 >> 2) as usize] as char);
21        out.push(ALPHABET[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
22        if chunk.len() > 1 {
23            out.push(ALPHABET[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
24        } else {
25            out.push('=');
26        }
27        if chunk.len() > 2 {
28            out.push(ALPHABET[(b2 & 0x3f) as usize] as char);
29        } else {
30            out.push('=');
31        }
32    }
33    out
34}
35
36/// Decodes standard base64 `text` back to bytes, failing closed on any invalid
37/// character, length, or padding.
38///
39/// Shared with `sim-codec-bitwise-base64` (see [`encode_base64`]).
40pub fn decode_base64(codec: CodecId, text: &str) -> Result<Vec<u8>> {
41    let clean = strip_ascii_whitespace(codec, text)?;
42    if !clean.len().is_multiple_of(4) {
43        return codec_err(codec, "invalid base64 length");
44    }
45
46    let mut out = Vec::with_capacity(clean.len() / 4 * 3);
47    let chunk_count = clean.len() / 4;
48    for (index, chunk) in clean.chunks_exact(4).enumerate() {
49        if chunk[0] == b'=' || chunk[1] == b'=' {
50            return codec_err(codec, "invalid base64 padding");
51        }
52
53        let is_last = index + 1 == chunk_count;
54        let c0 = decode_b64(codec, chunk[0])?;
55        let c1 = decode_b64(codec, chunk[1])?;
56        let c2 = decode_optional_b64(codec, chunk[2])?;
57        let c3 = decode_optional_b64(codec, chunk[3])?;
58
59        match (c2, c3) {
60            (Some(c2), Some(c3)) => {
61                out.push((c0 << 2) | (c1 >> 4));
62                out.push(((c1 & 0x0f) << 4) | (c2 >> 2));
63                out.push(((c2 & 0x03) << 6) | c3);
64            }
65            (Some(c2), None) => {
66                if !is_last {
67                    return codec_err(codec, "base64 padding before final quartet");
68                }
69                if c2 & 0x03 != 0 {
70                    return codec_err(codec, "non-zero base64 padding bits");
71                }
72                out.push((c0 << 2) | (c1 >> 4));
73                out.push(((c1 & 0x0f) << 4) | (c2 >> 2));
74            }
75            (None, None) => {
76                if !is_last {
77                    return codec_err(codec, "base64 padding before final quartet");
78                }
79                if c1 & 0x0f != 0 {
80                    return codec_err(codec, "non-zero base64 padding bits");
81                }
82                out.push((c0 << 2) | (c1 >> 4));
83            }
84            (None, Some(_)) => return codec_err(codec, "invalid base64 padding"),
85        }
86    }
87    Ok(out)
88}
89
90fn strip_ascii_whitespace(codec: CodecId, text: &str) -> Result<Vec<u8>> {
91    let mut clean = Vec::with_capacity(text.len());
92    for byte in text.bytes() {
93        match byte {
94            b' ' | b'\n' | b'\r' | b'\t' => {}
95            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'/' | b'=' => clean.push(byte),
96            _ => {
97                return Err(Error::CodecError {
98                    codec,
99                    message: format!("invalid base64 byte 0x{byte:02x}"),
100                });
101            }
102        }
103    }
104    Ok(clean)
105}
106
107fn decode_optional_b64(codec: CodecId, byte: u8) -> Result<Option<u8>> {
108    if byte == b'=' {
109        Ok(None)
110    } else {
111        decode_b64(codec, byte).map(Some)
112    }
113}
114
115fn decode_b64(codec: CodecId, byte: u8) -> Result<u8> {
116    match byte {
117        b'A'..=b'Z' => Ok(byte - b'A'),
118        b'a'..=b'z' => Ok(byte - b'a' + 26),
119        b'0'..=b'9' => Ok(byte - b'0' + 52),
120        b'+' => Ok(62),
121        b'/' => Ok(63),
122        _ => Err(Error::CodecError {
123            codec,
124            message: format!("invalid base64 byte 0x{byte:02x}"),
125        }),
126    }
127}
128
129fn codec_err<T>(codec: CodecId, message: impl Into<String>) -> Result<T> {
130    Err(Error::CodecError {
131        codec,
132        message: message.into(),
133    })
134}