bluetape_rs_codec/base64.rs
1//! Strict Base64 encoding and decoding.
2
3use std::error::Error;
4use std::fmt;
5
6use base64::Engine as _;
7use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD};
8
9/// Error returned when Base64 decoding rejects caller-owned input.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum Base64DecodeError {
13 /// The encoded text contains a byte outside the selected Base64 alphabet.
14 InvalidByte {
15 /// Zero-based byte position of the invalid byte.
16 index: usize,
17 /// Invalid byte value.
18 byte: u8,
19 },
20 /// The encoded text length is invalid for Base64 decoding.
21 InvalidLength {
22 /// Number of bytes in the encoded text.
23 len: usize,
24 },
25 /// The final non-padding symbol has impossible trailing bits.
26 InvalidLastSymbol {
27 /// Zero-based byte position of the invalid symbol.
28 index: usize,
29 /// Invalid byte value.
30 byte: u8,
31 },
32 /// The encoded text violates the selected padding policy.
33 InvalidPadding,
34}
35
36impl fmt::Display for Base64DecodeError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::InvalidByte { index, byte } => {
40 write!(
41 f,
42 "base64 input contains invalid byte 0x{byte:02x} at position {index}"
43 )
44 }
45 Self::InvalidLength { len } => {
46 write!(f, "base64 input has invalid byte length {len}")
47 }
48 Self::InvalidLastSymbol { index, byte } => {
49 write!(
50 f,
51 "base64 input contains invalid last symbol 0x{byte:02x} at position {index}"
52 )
53 }
54 Self::InvalidPadding => write!(f, "base64 input violates padding policy"),
55 }
56 }
57}
58
59impl Error for Base64DecodeError {}
60
61impl From<base64::DecodeError> for Base64DecodeError {
62 fn from(error: base64::DecodeError) -> Self {
63 match error {
64 base64::DecodeError::InvalidByte(index, byte) => Self::InvalidByte { index, byte },
65 base64::DecodeError::InvalidLength(len) => Self::InvalidLength { len },
66 base64::DecodeError::InvalidLastSymbol(index, byte) => {
67 Self::InvalidLastSymbol { index, byte }
68 }
69 base64::DecodeError::InvalidPadding => Self::InvalidPadding,
70 }
71 }
72}
73
74/// Encodes bytes with the standard Base64 alphabet and `=` padding.
75///
76/// # Examples
77///
78/// ```
79/// use bluetape_rs_codec::encode_base64;
80///
81/// assert_eq!(encode_base64(b"fo"), "Zm8=");
82/// ```
83#[must_use]
84pub fn encode_base64(bytes: impl AsRef<[u8]>) -> String {
85 STANDARD.encode(bytes)
86}
87
88/// Decodes standard Base64 text that uses the standard alphabet and padding.
89///
90/// # Examples
91///
92/// ```
93/// use bluetape_rs_codec::decode_base64;
94///
95/// assert_eq!(decode_base64("Zm8=")?, b"fo");
96/// # Ok::<(), bluetape_rs_codec::Base64DecodeError>(())
97/// ```
98///
99/// # Errors
100///
101/// Returns [`Base64DecodeError`] when input contains invalid bytes, invalid
102/// length, invalid trailing bits, or padding that does not match the standard
103/// padded policy.
104pub fn decode_base64(encoded: impl AsRef<str>) -> Result<Vec<u8>, Base64DecodeError> {
105 STANDARD.decode(encoded.as_ref()).map_err(Into::into)
106}
107
108/// Encodes bytes with the standard Base64 alphabet and no padding.
109///
110/// # Examples
111///
112/// ```
113/// use bluetape_rs_codec::encode_base64_unpadded;
114///
115/// assert_eq!(encode_base64_unpadded(b"fo"), "Zm8");
116/// ```
117#[must_use]
118pub fn encode_base64_unpadded(bytes: impl AsRef<[u8]>) -> String {
119 STANDARD_NO_PAD.encode(bytes)
120}
121
122/// Decodes standard Base64 text that must not contain padding.
123///
124/// # Examples
125///
126/// ```
127/// use bluetape_rs_codec::decode_base64_unpadded;
128///
129/// assert_eq!(decode_base64_unpadded("Zm8")?, b"fo");
130/// # Ok::<(), bluetape_rs_codec::Base64DecodeError>(())
131/// ```
132///
133/// # Errors
134///
135/// Returns [`Base64DecodeError`] when input contains invalid bytes, invalid
136/// length, invalid trailing bits, or padding in a no-padding variant.
137pub fn decode_base64_unpadded(encoded: impl AsRef<str>) -> Result<Vec<u8>, Base64DecodeError> {
138 STANDARD_NO_PAD.decode(encoded.as_ref()).map_err(Into::into)
139}
140
141/// Encodes bytes with the URL-safe Base64 alphabet and `=` padding.
142///
143/// # Examples
144///
145/// ```
146/// use bluetape_rs_codec::encode_base64_url;
147///
148/// assert_eq!(encode_base64_url([0xfb, 0xff]), "-_8=");
149/// ```
150#[must_use]
151pub fn encode_base64_url(bytes: impl AsRef<[u8]>) -> String {
152 URL_SAFE.encode(bytes)
153}
154
155/// Decodes URL-safe Base64 text that uses `=` padding.
156///
157/// # Examples
158///
159/// ```
160/// use bluetape_rs_codec::decode_base64_url;
161///
162/// assert_eq!(decode_base64_url("-_8=")?, vec![0xfb, 0xff]);
163/// # Ok::<(), bluetape_rs_codec::Base64DecodeError>(())
164/// ```
165///
166/// # Errors
167///
168/// Returns [`Base64DecodeError`] when input contains invalid bytes, invalid
169/// length, invalid trailing bits, or padding that does not match the URL-safe
170/// padded policy.
171pub fn decode_base64_url(encoded: impl AsRef<str>) -> Result<Vec<u8>, Base64DecodeError> {
172 URL_SAFE.decode(encoded.as_ref()).map_err(Into::into)
173}
174
175/// Encodes bytes with the URL-safe Base64 alphabet and no padding.
176///
177/// # Examples
178///
179/// ```
180/// use bluetape_rs_codec::encode_base64_url_unpadded;
181///
182/// assert_eq!(encode_base64_url_unpadded([0xfb, 0xff]), "-_8");
183/// ```
184#[must_use]
185pub fn encode_base64_url_unpadded(bytes: impl AsRef<[u8]>) -> String {
186 URL_SAFE_NO_PAD.encode(bytes)
187}
188
189/// Decodes URL-safe Base64 text that must not contain padding.
190///
191/// # Examples
192///
193/// ```
194/// use bluetape_rs_codec::decode_base64_url_unpadded;
195///
196/// assert_eq!(decode_base64_url_unpadded("-_8")?, vec![0xfb, 0xff]);
197/// # Ok::<(), bluetape_rs_codec::Base64DecodeError>(())
198/// ```
199///
200/// # Errors
201///
202/// Returns [`Base64DecodeError`] when input contains invalid bytes, invalid
203/// length, invalid trailing bits, or padding in a no-padding variant.
204pub fn decode_base64_url_unpadded(encoded: impl AsRef<str>) -> Result<Vec<u8>, Base64DecodeError> {
205 URL_SAFE_NO_PAD.decode(encoded.as_ref()).map_err(Into::into)
206}