Skip to content

Commit 2509fd2

Browse files
committed
Add named Base64 profiles
1 parent 28141ec commit 2509fd2

5 files changed

Lines changed: 368 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
configured line ending and enforce configured-width non-final lines.
1414
- Added custom alphabet helpers: `validate_alphabet`,
1515
`decode_alphabet_byte`, and `AlphabetError`.
16+
- Added named dependency-free profiles for MIME, PEM, bcrypt-style, and
17+
`crypt(3)`-style Base64 through `Profile`, `MIME`, `PEM`, `PEM_CRLF`,
18+
`BCRYPT`, and `CRYPT`.
1619

1720
## 0.5.0 - 2026-05-14
1821

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Implemented now:
2929
- Strict line-wrapped validation and decoding profiles for MIME/PEM-style
3030
input.
3131
- Custom alphabet validation helpers for user-defined 64-byte alphabets.
32+
- Named dependency-free profiles for MIME, PEM, bcrypt-style, and
33+
`crypt(3)`-style Base64.
3234
- Separate `ct` scalar decode module for sensitive payloads that avoids
3335
secret-indexed lookup tables during Base64 symbol mapping.
3436
- `std::io` streaming encoders and decoders behind the `stream` feature.
@@ -182,6 +184,20 @@ Built-in policies include `LineWrap::MIME`, `LineWrap::PEM`, and
182184
`LineWrap::PEM_CRLF`. Wrapping inserts line endings between encoded lines and
183185
does not append a trailing line ending after the final line.
184186

187+
Named profiles carry the wrapping policy for common protocols:
188+
189+
```rust
190+
use base64_ng::{MIME, PEM};
191+
192+
assert_eq!(MIME.line_wrap().unwrap().line_len, 76);
193+
assert_eq!(PEM.line_wrap().unwrap().line_len, 64);
194+
195+
let mut encoded = [0u8; 82];
196+
let written = MIME.encode_slice(&[0x5a; 58], &mut encoded).unwrap();
197+
assert_eq!(&encoded[76..78], b"\r\n");
198+
assert!(MIME.validate(&encoded[..written]));
199+
```
200+
185201
The same policy can be used for strict wrapped decoding. Unlike legacy
186202
whitespace decoding, this accepts only the configured line ending and requires
187203
every non-final line to have the configured encoded length:
@@ -220,6 +236,23 @@ validate_alphabet(&DotSlash::ENCODE).unwrap();
220236
assert_eq!(DotSlash::decode(b'.'), Some(0));
221237
```
222238

239+
Built-in non-RFC alphabets are available for explicit interoperability:
240+
241+
```rust
242+
use base64_ng::{BCRYPT, CRYPT};
243+
244+
let mut bcrypt = [0u8; 4];
245+
let written = BCRYPT.encode_slice(&[0xff, 0xff, 0xff], &mut bcrypt).unwrap();
246+
assert_eq!(&bcrypt[..written], b"9999");
247+
248+
let mut crypt = [0u8; 4];
249+
let written = CRYPT.encode_slice(&[0xff, 0xff, 0xff], &mut crypt).unwrap();
250+
assert_eq!(&crypt[..written], b"zzzz");
251+
```
252+
253+
The bcrypt and `crypt(3)` profiles provide alphabets and no-padding behavior
254+
only. They do not parse or verify complete password-hash strings.
255+
223256
## Legacy Whitespace Decoding
224257

225258
Strict decoding rejects whitespace. If an existing protocol allows line-wrapped

docs/PLAN.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -171,22 +171,12 @@ the zero-runtime-dependency stance.
171171
- Const encode support into caller-sized arrays.
172172
- Runtime backend reports and high-assurance scalar-only policy checks.
173173
- Reserved SIMD backends that must prove scalar equivalence before admission.
174+
- Named MIME, PEM, bcrypt-style, and `crypt(3)`-style profiles.
175+
- Custom alphabet validation helpers with duplicate-character, padding-byte,
176+
and visible-ASCII checks.
174177

175178
### Missing Secure Core Features
176179

177-
- Add built-in MIME and PEM profiles with strict line-length handling, custom
178-
line endings, and profile-specific whitespace policy.
179-
- Add a built-in bcrypt alphabet/profile if its non-RFC behavior can be
180-
represented clearly without weakening RFC 4648 defaults.
181-
- Add custom alphabet/profile constructors with duplicate-character rejection,
182-
padding-character conflict checks, and deterministic error reporting.
183-
- Add line-wrapping encode APIs for 64-column PEM, 76-column MIME, and
184-
caller-selected line endings.
185-
- Add validate-only APIs:
186-
- `validate(input) -> bool` for quick sanitization.
187-
- `validate_result(input) -> Result<(), DecodeError>` for diagnostics.
188-
- profile-aware validation for strict, legacy, MIME, PEM, and constant-time
189-
paths.
190180
- Add zero-dependency small-output helpers for common short values, such as a
191181
stack-backed encoded buffer type that avoids heap allocation without forcing
192182
callers to hand-roll arrays.

src/lib.rs

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,18 @@ pub const URL_SAFE: Engine<UrlSafe, true> = Engine::new();
14311431
/// URL-safe Base64 engine without padding.
14321432
pub const URL_SAFE_NO_PAD: Engine<UrlSafe, false> = Engine::new();
14331433

1434+
/// bcrypt-style Base64 engine without padding.
1435+
///
1436+
/// This uses the bcrypt alphabet with the crate's normal Base64 bit packing.
1437+
/// It does not parse complete bcrypt password-hash strings.
1438+
pub const BCRYPT_NO_PAD: Engine<Bcrypt, false> = Engine::new();
1439+
1440+
/// Unix `crypt(3)`-style Base64 engine without padding.
1441+
///
1442+
/// This uses the `crypt(3)` alphabet with the crate's normal Base64 bit
1443+
/// packing. It does not parse complete password-hash strings.
1444+
pub const CRYPT_NO_PAD: Engine<Crypt, false> = Engine::new();
1445+
14341446
/// Line ending used by wrapped Base64 output.
14351447
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14361448
pub enum LineEnding {
@@ -1491,6 +1503,164 @@ impl LineWrap {
14911503
}
14921504
}
14931505

1506+
/// A named Base64 profile with an engine and optional strict line wrapping.
1507+
///
1508+
/// Profiles are convenience values for protocol-shaped Base64. They keep the
1509+
/// same strict alphabet, padding, canonical-bit, and output-buffer rules as
1510+
/// [`Engine`], while carrying the wrapping policy for MIME/PEM-like formats.
1511+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1512+
pub struct Profile<A, const PAD: bool> {
1513+
engine: Engine<A, PAD>,
1514+
wrap: Option<LineWrap>,
1515+
}
1516+
1517+
impl<A, const PAD: bool> Profile<A, PAD>
1518+
where
1519+
A: Alphabet,
1520+
{
1521+
/// Creates a profile from an engine and optional strict line wrapping.
1522+
#[must_use]
1523+
pub const fn new(engine: Engine<A, PAD>, wrap: Option<LineWrap>) -> Self {
1524+
Self { engine, wrap }
1525+
}
1526+
1527+
/// Returns the underlying engine.
1528+
#[must_use]
1529+
pub const fn engine(&self) -> Engine<A, PAD> {
1530+
self.engine
1531+
}
1532+
1533+
/// Returns the strict wrapping policy carried by this profile, if any.
1534+
#[must_use]
1535+
pub const fn line_wrap(&self) -> Option<LineWrap> {
1536+
self.wrap
1537+
}
1538+
1539+
/// Returns the encoded length for this profile.
1540+
pub const fn encoded_len(&self, input_len: usize) -> Result<usize, EncodeError> {
1541+
match self.wrap {
1542+
Some(wrap) => wrapped_encoded_len(input_len, PAD, wrap),
1543+
None => encoded_len(input_len, PAD),
1544+
}
1545+
}
1546+
1547+
/// Returns the exact decoded length for this profile.
1548+
pub fn decoded_len(&self, input: &[u8]) -> Result<usize, DecodeError> {
1549+
match self.wrap {
1550+
Some(wrap) => self.engine.decoded_len_wrapped(input, wrap),
1551+
None => self.engine.decoded_len(input),
1552+
}
1553+
}
1554+
1555+
/// Validates input according to this profile without writing decoded bytes.
1556+
pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError> {
1557+
match self.wrap {
1558+
Some(wrap) => self.engine.validate_wrapped_result(input, wrap),
1559+
None => self.engine.validate_result(input),
1560+
}
1561+
}
1562+
1563+
/// Returns whether `input` is valid for this profile.
1564+
#[must_use]
1565+
pub fn validate(&self, input: &[u8]) -> bool {
1566+
self.validate_result(input).is_ok()
1567+
}
1568+
1569+
/// Encodes `input` into `output` according to this profile.
1570+
pub fn encode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, EncodeError> {
1571+
match self.wrap {
1572+
Some(wrap) => self.engine.encode_slice_wrapped(input, output, wrap),
1573+
None => self.engine.encode_slice(input, output),
1574+
}
1575+
}
1576+
1577+
/// Encodes `input` into `output` and clears all bytes after the encoded
1578+
/// prefix.
1579+
pub fn encode_slice_clear_tail(
1580+
&self,
1581+
input: &[u8],
1582+
output: &mut [u8],
1583+
) -> Result<usize, EncodeError> {
1584+
match self.wrap {
1585+
Some(wrap) => self
1586+
.engine
1587+
.encode_slice_wrapped_clear_tail(input, output, wrap),
1588+
None => self.engine.encode_slice_clear_tail(input, output),
1589+
}
1590+
}
1591+
1592+
/// Decodes `input` into `output` according to this profile.
1593+
pub fn decode_slice(&self, input: &[u8], output: &mut [u8]) -> Result<usize, DecodeError> {
1594+
match self.wrap {
1595+
Some(wrap) => self.engine.decode_slice_wrapped(input, output, wrap),
1596+
None => self.engine.decode_slice(input, output),
1597+
}
1598+
}
1599+
1600+
/// Decodes `input` into `output` and clears all bytes after the decoded
1601+
/// prefix.
1602+
pub fn decode_slice_clear_tail(
1603+
&self,
1604+
input: &[u8],
1605+
output: &mut [u8],
1606+
) -> Result<usize, DecodeError> {
1607+
match self.wrap {
1608+
Some(wrap) => self
1609+
.engine
1610+
.decode_slice_wrapped_clear_tail(input, output, wrap),
1611+
None => self.engine.decode_slice_clear_tail(input, output),
1612+
}
1613+
}
1614+
1615+
/// Encodes `input` into a newly allocated byte vector.
1616+
#[cfg(feature = "alloc")]
1617+
pub fn encode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, EncodeError> {
1618+
match self.wrap {
1619+
Some(wrap) => self.engine.encode_wrapped_vec(input, wrap),
1620+
None => self.engine.encode_vec(input),
1621+
}
1622+
}
1623+
1624+
/// Encodes `input` into a newly allocated UTF-8 string.
1625+
#[cfg(feature = "alloc")]
1626+
pub fn encode_string(&self, input: &[u8]) -> Result<alloc::string::String, EncodeError> {
1627+
match self.wrap {
1628+
Some(wrap) => self.engine.encode_wrapped_string(input, wrap),
1629+
None => self.engine.encode_string(input),
1630+
}
1631+
}
1632+
1633+
/// Decodes `input` into a newly allocated byte vector.
1634+
#[cfg(feature = "alloc")]
1635+
pub fn decode_vec(&self, input: &[u8]) -> Result<alloc::vec::Vec<u8>, DecodeError> {
1636+
match self.wrap {
1637+
Some(wrap) => self.engine.decode_wrapped_vec(input, wrap),
1638+
None => self.engine.decode_vec(input),
1639+
}
1640+
}
1641+
}
1642+
1643+
/// MIME Base64 profile: standard alphabet, padding, 76-column CRLF wrapping.
1644+
pub const MIME: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::MIME));
1645+
1646+
/// PEM Base64 profile: standard alphabet, padding, 64-column LF wrapping.
1647+
pub const PEM: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM));
1648+
1649+
/// PEM Base64 profile with CRLF line endings.
1650+
pub const PEM_CRLF: Profile<Standard, true> = Profile::new(STANDARD, Some(LineWrap::PEM_CRLF));
1651+
1652+
/// bcrypt-style no-padding Base64 profile.
1653+
///
1654+
/// This profile carries the bcrypt alphabet and no padding. It does not parse
1655+
/// complete bcrypt password-hash strings.
1656+
pub const BCRYPT: Profile<Bcrypt, false> = Profile::new(BCRYPT_NO_PAD, None);
1657+
1658+
/// Unix `crypt(3)`-style no-padding Base64 profile.
1659+
///
1660+
/// This profile carries the `crypt(3)` alphabet and no padding. It does not
1661+
/// parse complete password-hash strings.
1662+
pub const CRYPT: Profile<Crypt, false> = Profile::new(CRYPT_NO_PAD, None);
1663+
14941664
/// Returns the encoded length for an input length and padding policy.
14951665
///
14961666
/// This function returns [`EncodeError::LengthOverflow`] instead of panicking.
@@ -1761,6 +1931,39 @@ impl Alphabet for UrlSafe {
17611931
}
17621932
}
17631933

1934+
/// The bcrypt Base64 alphabet.
1935+
///
1936+
/// This alphabet is commonly used by bcrypt hash strings. It is provided as an
1937+
/// alphabet/profile building block; `base64-ng` does not parse or verify full
1938+
/// bcrypt password-hash records.
1939+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1940+
pub struct Bcrypt;
1941+
1942+
impl Alphabet for Bcrypt {
1943+
const ENCODE: [u8; 64] = *b"./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1944+
1945+
#[inline]
1946+
fn decode(byte: u8) -> Option<u8> {
1947+
decode_alphabet_byte(byte, &Self::ENCODE)
1948+
}
1949+
}
1950+
1951+
/// The Unix `crypt(3)` Base64 alphabet.
1952+
///
1953+
/// This alphabet is provided as an explicit legacy interoperability profile.
1954+
/// `base64-ng` does not parse or verify complete password-hash records.
1955+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1956+
pub struct Crypt;
1957+
1958+
impl Alphabet for Crypt {
1959+
const ENCODE: [u8; 64] = *b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1960+
1961+
#[inline]
1962+
fn decode(byte: u8) -> Option<u8> {
1963+
decode_alphabet_byte(byte, &Self::ENCODE)
1964+
}
1965+
}
1966+
17641967
#[inline]
17651968
const fn encode_base64_value<A: Alphabet>(value: u8) -> u8 {
17661969
encode_alphabet_value(value, &A::ENCODE)
@@ -1984,11 +2187,43 @@ mod backend {
19842187
}
19852188

19862189
/// A zero-sized Base64 engine parameterized by alphabet and padding policy.
1987-
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
19882190
pub struct Engine<A, const PAD: bool> {
19892191
alphabet: core::marker::PhantomData<A>,
19902192
}
19912193

2194+
impl<A, const PAD: bool> Clone for Engine<A, PAD> {
2195+
fn clone(&self) -> Self {
2196+
*self
2197+
}
2198+
}
2199+
2200+
impl<A, const PAD: bool> Copy for Engine<A, PAD> {}
2201+
2202+
impl<A, const PAD: bool> core::fmt::Debug for Engine<A, PAD> {
2203+
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2204+
formatter
2205+
.debug_struct("Engine")
2206+
.field("padded", &PAD)
2207+
.finish()
2208+
}
2209+
}
2210+
2211+
impl<A, const PAD: bool> Default for Engine<A, PAD> {
2212+
fn default() -> Self {
2213+
Self {
2214+
alphabet: core::marker::PhantomData,
2215+
}
2216+
}
2217+
}
2218+
2219+
impl<A, const PAD: bool> Eq for Engine<A, PAD> {}
2220+
2221+
impl<A, const PAD: bool> PartialEq for Engine<A, PAD> {
2222+
fn eq(&self, _other: &Self) -> bool {
2223+
true
2224+
}
2225+
}
2226+
19922227
impl<A, const PAD: bool> Engine<A, PAD>
19932228
where
19942229
A: Alphabet,

0 commit comments

Comments
 (0)