Skip to main content

multi_base/
encoded.rs

1// SPDX-License-Identifier: MIT
2
3//! Validated multibase-encoded strings.
4//!
5//! This module provides the [`EncodedString`] type, a newtype wrapper that guarantees
6//! a string is a valid multibase-encoded value with a recognized base code prefix.
7//!
8//! # Type Safety
9//!
10//! Following the "parse, don't validate" principle, [`EncodedString`] ensures that
11//! once created, the string is guaranteed to:
12//! - Have a valid base code prefix
13//! - Be parseable (though decoding may still fail if the data is malformed)
14//!
15//! # Examples
16//!
17//! ```
18//! use multi_base::{EncodedString, Base};
19//!
20//! // Parse and validate a multibase string
21//! let encoded = EncodedString::new("zCn8eVZg").unwrap();
22//! assert_eq!(encoded.base(), Base::Base58Btc);
23//!
24//! // Decode the validated string
25//! let decoded = encoded.decode().unwrap();
26//! assert_eq!(decoded, b"hello");
27//!
28//! // Access the underlying string
29//! assert_eq!(encoded.as_str(), "zCn8eVZg");
30//! ```
31
32#[cfg(not(feature = "std"))]
33use alloc::{string::String, vec::Vec};
34
35use crate::{Base, Error, Result};
36
37/// A validated multibase-encoded string.
38///
39/// This type provides type-level guarantees that a string contains a valid
40/// base code prefix and can be decoded (though decoding may still fail if
41/// the data is malformed).
42///
43/// # Construction
44///
45/// Use [`EncodedString::new`] or parse from a string using [`core::str::FromStr`]:
46///
47/// ```
48/// use multi_base::EncodedString;
49/// use std::str::FromStr;
50///
51/// // Using new()
52/// let encoded = EncodedString::new("zCn8eVZg").unwrap();
53///
54/// // Using FromStr
55/// let encoded: EncodedString = "md29ybGQ".parse().unwrap();
56/// ```
57///
58/// # Errors
59///
60/// Construction fails if:
61/// - The input string is empty
62/// - The first character is not a valid base code
63///
64/// Note that construction only validates the base code prefix, not the
65/// encoded data itself. Use [`decode()`](Self::decode) to fully validate
66/// and decode the data.
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct EncodedString {
69    /// The validated base encoding.
70    base: Base,
71    /// The complete encoded string (including base code prefix).
72    inner: String,
73}
74
75impl EncodedString {
76    /// Create a new validated multibase-encoded string.
77    ///
78    /// This function validates that the input string has a valid base code
79    /// prefix. It does not decode the full data, so malformed encoded data
80    /// will only be detected when calling [`decode()`](Self::decode).
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use multi_base::{EncodedString, Base};
86    ///
87    /// // Valid multibase string
88    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
89    /// assert_eq!(encoded.base(), Base::Base58Btc);
90    ///
91    /// // Empty string fails
92    /// assert!(EncodedString::new("").is_err());
93    ///
94    /// // Invalid base code fails
95    /// assert!(EncodedString::new("xInvalid").is_err());
96    /// ```
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if:
101    /// - The input string is empty ([`Error::EmptyInput`])
102    /// - The first character is not a valid base code ([`Error::UnknownBase`])
103    pub fn new<S: Into<String>>(s: S) -> Result<Self> {
104        let inner = s.into();
105
106        // Validate base code prefix
107        let code = inner.chars().next().ok_or(Error::EmptyInput)?;
108        let base = Base::from_code(code)?;
109
110        Ok(Self { base, inner })
111    }
112
113    /// Get the base encoding of this string.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use multi_base::{EncodedString, Base};
119    ///
120    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
121    /// assert_eq!(encoded.base(), Base::Base58Btc);
122    ///
123    /// let encoded = EncodedString::new("md29ybGQ").unwrap();
124    /// assert_eq!(encoded.base(), Base::Base64);
125    /// ```
126    #[inline]
127    #[must_use]
128    pub const fn base(&self) -> Base {
129        self.base
130    }
131
132    /// Get the encoded string as a string slice.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use multi_base::EncodedString;
138    ///
139    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
140    /// assert_eq!(encoded.as_str(), "zCn8eVZg");
141    /// ```
142    #[inline]
143    #[must_use]
144    pub fn as_str(&self) -> &str {
145        &self.inner
146    }
147
148    /// Decode this validated multibase string.
149    ///
150    /// This performs the actual decoding of the data. Unlike construction,
151    /// which only validates the base code prefix, this method validates and
152    /// decodes the entire encoded data.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use multi_base::EncodedString;
158    ///
159    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
160    /// let decoded = encoded.decode().unwrap();
161    /// assert_eq!(decoded, b"hello");
162    /// ```
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if the encoded data is malformed for the detected base.
167    #[inline]
168    pub fn decode(&self) -> Result<Vec<u8>> {
169        let code_len = self.base.code().len_utf8();
170        self.base.decode(&self.inner[code_len..], true)
171    }
172
173    /// Decode this validated multibase string with strictness control.
174    ///
175    /// When `strict` is `false`, some bases allow case-insensitive decoding
176    /// or other permissive behaviors.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use multi_base::EncodedString;
182    ///
183    /// // Case-insensitive decoding for some bases
184    /// let encoded = EncodedString::new("FaB").unwrap(); // Base16Upper with mixed case
185    /// let decoded = encoded.decode_with_strictness(false).unwrap();
186    /// assert_eq!(decoded, b"\xab");
187    /// ```
188    ///
189    /// # Errors
190    ///
191    /// Returns an error if the encoded data is malformed for the detected base.
192    #[inline]
193    pub fn decode_with_strictness(&self, strict: bool) -> Result<Vec<u8>> {
194        let code_len = self.base.code().len_utf8();
195        self.base.decode(&self.inner[code_len..], strict)
196    }
197
198    /// Consume the `EncodedString` and return the inner string.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use multi_base::EncodedString;
204    ///
205    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
206    /// let inner = encoded.into_inner();
207    /// assert_eq!(inner, "zCn8eVZg");
208    /// ```
209    #[must_use]
210    pub fn into_inner(self) -> String {
211        self.inner
212    }
213}
214
215// Implement AsRef<str> for ergonomic usage
216impl AsRef<str> for EncodedString {
217    fn as_ref(&self) -> &str {
218        &self.inner
219    }
220}
221
222// Implement Display for easy printing
223impl core::fmt::Display for EncodedString {
224    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
225        write!(f, "{}", self.inner)
226    }
227}
228
229// Implement FromStr for parsing
230impl core::str::FromStr for EncodedString {
231    type Err = Error;
232
233    fn from_str(s: &str) -> Result<Self> {
234        Self::new(s)
235    }
236}
237
238// Implement TryFrom<String> for convenient conversion
239impl core::convert::TryFrom<String> for EncodedString {
240    type Error = Error;
241
242    fn try_from(s: String) -> Result<Self> {
243        Self::new(s)
244    }
245}
246
247// Implement TryFrom<&str> for convenient conversion
248impl<'a> core::convert::TryFrom<&'a str> for EncodedString {
249    type Error = Error;
250
251    fn try_from(s: &'a str) -> Result<Self> {
252        Self::new(s)
253    }
254}
255
256// Implement From<EncodedString> for String for easy unwrapping
257impl From<EncodedString> for String {
258    fn from(encoded: EncodedString) -> Self {
259        encoded.inner
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_new_valid() {
269        let encoded = EncodedString::new("zCn8eVZg").unwrap();
270        assert_eq!(encoded.base(), Base::Base58Btc);
271        assert_eq!(encoded.as_str(), "zCn8eVZg");
272    }
273
274    #[test]
275    fn test_new_empty_fails() {
276        let result = EncodedString::new("");
277        assert!(result.is_err());
278        assert!(matches!(result.unwrap_err(), Error::EmptyInput));
279    }
280
281    #[test]
282    fn test_new_invalid_base_fails() {
283        let result = EncodedString::new("xInvalid");
284        assert!(result.is_err());
285        assert!(matches!(result.unwrap_err(), Error::UnknownBase { .. }));
286    }
287
288    #[test]
289    fn test_decode() {
290        let encoded = EncodedString::new("zCn8eVZg").unwrap();
291        let decoded = encoded.decode().unwrap();
292        assert_eq!(decoded, b"hello");
293    }
294
295    #[test]
296    fn test_from_str() {
297        use core::str::FromStr;
298        let encoded = EncodedString::from_str("md29ybGQ").unwrap();
299        assert_eq!(encoded.base(), Base::Base64);
300    }
301
302    #[test]
303    fn test_try_from_string() {
304        use core::convert::TryFrom;
305        let encoded = EncodedString::try_from("f48656c6c6f".to_string()).unwrap();
306        assert_eq!(encoded.base(), Base::Base16Lower);
307    }
308
309    #[test]
310    fn test_try_from_str() {
311        use core::convert::TryFrom;
312        let encoded = EncodedString::try_from("BPFSXGIDNMFXGSIBB").unwrap();
313        assert_eq!(encoded.base(), Base::Base32Upper);
314    }
315
316    #[test]
317    fn test_into_inner() {
318        let encoded = EncodedString::new("zCn8eVZg").unwrap();
319        let inner = encoded.into_inner();
320        assert_eq!(inner, "zCn8eVZg");
321    }
322
323    #[test]
324    fn test_display() {
325        let encoded = EncodedString::new("zCn8eVZg").unwrap();
326        assert_eq!(format!("{encoded}"), "zCn8eVZg");
327    }
328
329    #[test]
330    fn test_clone() {
331        let encoded = EncodedString::new("zCn8eVZg").unwrap();
332        let cloned = encoded.clone();
333        assert_eq!(encoded, cloned);
334    }
335
336    #[test]
337    fn test_eq() {
338        let encoded1 = EncodedString::new("zCn8eVZg").unwrap();
339        let encoded2 = EncodedString::new("zCn8eVZg").unwrap();
340        let encoded3 = EncodedString::new("md29ybGQ").unwrap();
341
342        assert_eq!(encoded1, encoded2);
343        assert_ne!(encoded1, encoded3);
344    }
345
346    #[test]
347    fn test_as_ref() {
348        let encoded = EncodedString::new("zCn8eVZg").unwrap();
349        let s: &str = encoded.as_ref();
350        assert_eq!(s, "zCn8eVZg");
351    }
352}