Skip to main content

byte_wrapper/
base64_slice.rs

1// Copyright (c) The byte-wrapper Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! The [`Base64Slice`] newtype wrapper.
5
6use base64::{
7    display::Base64Display,
8    encoded_len,
9    engine::general_purpose::{GeneralPurpose, STANDARD},
10};
11use core::fmt::{self, Write};
12
13/// A byte slice that displays as base64.
14///
15/// The borrowed, dynamically-sized equivalent to
16/// [`Base64Vec`](crate::Base64Vec), akin to `str`'s relationship to `String`.
17///
18/// `Base64Slice` wraps `[u8]`, providing a [`Display`](fmt::Display)
19/// implementation that uses base64 encoding.
20///
21/// With the **`serde`** feature enabled, it also implements
22/// `Serialize` (base64 strings in human-readable formats, raw bytes in
23/// binary formats).
24///
25/// # Examples
26///
27/// ```
28/// use byte_wrapper::Base64Slice;
29///
30/// const BYTES: [u8; 3] = [1, 2, 3];
31/// const B: &Base64Slice = Base64Slice::new(&BYTES);
32/// assert_eq!(B.to_string(), "AQID");
33/// ```
34#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
35#[repr(transparent)]
36pub struct Base64Slice([u8]);
37
38impl Base64Slice {
39    /// Creates a new `Base64Slice` from a byte slice.
40    #[inline]
41    #[must_use]
42    pub const fn new(bytes: &[u8]) -> &Self {
43        // SAFETY:
44        //
45        // * `repr(transparent)` gives `Base64Slice` the layout and validity of
46        //   `[u8]`.
47        // * An unsized-to-unsized pointer cast retains the length metadata and
48        //   provenance.
49        // * The output borrow inherits the input's lifetime and shared-ness,
50        //   and `Base64Slice` has no interior mutability.
51        unsafe { &*(bytes as *const [u8] as *const Self) }
52    }
53
54    /// Returns the inner byte slice.
55    #[inline]
56    #[must_use]
57    pub const fn as_bytes(&self) -> &[u8] {
58        &self.0
59    }
60}
61
62fn base64_display(bytes: &[u8]) -> Base64Display<'_, 'static, GeneralPurpose> {
63    Base64Display::new(bytes, &STANDARD)
64}
65
66/// Formats bytes as a quoted base64 string without allocating.
67///
68/// This matches `String`'s `Debug` impl, since base64 never needs escaping.
69pub(crate) struct QuotedBase64<'a>(pub(crate) &'a [u8]);
70
71impl fmt::Debug for QuotedBase64<'_> {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.write_char('"')?;
74        fmt::Display::fmt(&base64_display(self.0), f)?;
75        f.write_char('"')
76    }
77}
78
79/// Forwards at most `remaining` bytes and drops the rest.
80///
81/// base64 is ASCII, so bytes and chars are the same.
82struct Truncate<'a, 'b> {
83    inner: &'a mut fmt::Formatter<'b>,
84    remaining: usize,
85}
86
87impl Write for Truncate<'_, '_> {
88    fn write_str(&mut self, s: &str) -> fmt::Result {
89        let take = s.len().min(self.remaining);
90        self.remaining -= take;
91        self.inner.write_str(&s[..take])
92    }
93}
94
95impl fmt::Debug for Base64Slice {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_tuple("Base64Slice").field(&QuotedBase64(&self.0)).finish()
98    }
99}
100
101impl fmt::Display for Base64Slice {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        // Implement the displayer by hand, matching how str computes width,
104        // fill, etc. We can't use String because this displayer is available in
105        // no-alloc contexts.
106        let encoded_len = encoded_len(self.0.len(), true)
107            .expect("base64 length of an in-memory slice fits in usize");
108        let content_len = match f.precision() {
109            Some(precision) => precision.min(encoded_len),
110            None => encoded_len,
111        };
112        let padding = match f.width() {
113            Some(width) => width.saturating_sub(content_len),
114            None => 0,
115        };
116        let (pre, post) = match f.align() {
117            Some(fmt::Alignment::Left) | None => (0, padding),
118            Some(fmt::Alignment::Right) => (padding, 0),
119            Some(fmt::Alignment::Center) => {
120                (padding / 2, padding - padding / 2)
121            }
122        };
123
124        let fill = f.fill();
125        for _ in 0..pre {
126            f.write_char(fill)?;
127        }
128        if content_len == encoded_len {
129            fmt::Display::fmt(&base64_display(&self.0), f)?;
130        } else {
131            let mut truncate = Truncate { inner: f, remaining: content_len };
132            write!(truncate, "{}", base64_display(&self.0))?;
133        }
134        for _ in 0..post {
135            f.write_char(fill)?;
136        }
137        Ok(())
138    }
139}
140
141impl core::ops::Deref for Base64Slice {
142    type Target = [u8];
143
144    #[inline]
145    fn deref(&self) -> &Self::Target {
146        &self.0
147    }
148}
149
150impl AsRef<[u8]> for Base64Slice {
151    #[inline]
152    fn as_ref(&self) -> &[u8] {
153        &self.0
154    }
155}
156
157impl AsRef<Base64Slice> for Base64Slice {
158    #[inline]
159    fn as_ref(&self) -> &Base64Slice {
160        self
161    }
162}
163
164#[cfg(feature = "serde")]
165mod serde_impls {
166    use super::{Base64Slice, base64_display};
167
168    #[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
169    impl serde_core::Serialize for Base64Slice {
170        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
171        where
172            S: serde_core::Serializer,
173        {
174            if serializer.is_human_readable() {
175                serializer.collect_str(&base64_display(&self.0))
176            } else {
177                serializer.serialize_bytes(&self.0)
178            }
179        }
180    }
181}