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    pub fn base(&self) -> Base {
127        self.base
128    }
129
130    /// Get the encoded string as a string slice.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use multi_base::EncodedString;
136    ///
137    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
138    /// assert_eq!(encoded.as_str(), "zCn8eVZg");
139    /// ```
140    pub fn as_str(&self) -> &str {
141        &self.inner
142    }
143
144    /// Decode this validated multibase string.
145    ///
146    /// This performs the actual decoding of the data. Unlike construction,
147    /// which only validates the base code prefix, this method validates and
148    /// decodes the entire encoded data.
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use multi_base::EncodedString;
154    ///
155    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
156    /// let decoded = encoded.decode().unwrap();
157    /// assert_eq!(decoded, b"hello");
158    /// ```
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the encoded data is malformed for the detected base.
163    pub fn decode(&self) -> Result<Vec<u8>> {
164        let code_len = self.base.code().len_utf8();
165        self.base.decode(&self.inner[code_len..], true)
166    }
167
168    /// Decode this validated multibase string with strictness control.
169    ///
170    /// When `strict` is `false`, some bases allow case-insensitive decoding
171    /// or other permissive behaviors.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use multi_base::EncodedString;
177    ///
178    /// // Case-insensitive decoding for some bases
179    /// let encoded = EncodedString::new("FaB").unwrap(); // Base16Upper with mixed case
180    /// let decoded = encoded.decode_with_strictness(false).unwrap();
181    /// assert_eq!(decoded, b"\xab");
182    /// ```
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if the encoded data is malformed for the detected base.
187    pub fn decode_with_strictness(&self, strict: bool) -> Result<Vec<u8>> {
188        let code_len = self.base.code().len_utf8();
189        self.base.decode(&self.inner[code_len..], strict)
190    }
191
192    /// Consume the `EncodedString` and return the inner string.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use multi_base::EncodedString;
198    ///
199    /// let encoded = EncodedString::new("zCn8eVZg").unwrap();
200    /// let inner = encoded.into_inner();
201    /// assert_eq!(inner, "zCn8eVZg");
202    /// ```
203    pub fn into_inner(self) -> String {
204        self.inner
205    }
206}
207
208// Implement AsRef<str> for ergonomic usage
209impl AsRef<str> for EncodedString {
210    fn as_ref(&self) -> &str {
211        &self.inner
212    }
213}
214
215// Implement Display for easy printing
216impl core::fmt::Display for EncodedString {
217    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218        write!(f, "{}", self.inner)
219    }
220}
221
222// Implement FromStr for parsing
223impl core::str::FromStr for EncodedString {
224    type Err = Error;
225
226    fn from_str(s: &str) -> Result<Self> {
227        Self::new(s)
228    }
229}
230
231// Implement TryFrom<String> for convenient conversion
232impl core::convert::TryFrom<String> for EncodedString {
233    type Error = Error;
234
235    fn try_from(s: String) -> Result<Self> {
236        Self::new(s)
237    }
238}
239
240// Implement TryFrom<&str> for convenient conversion
241impl<'a> core::convert::TryFrom<&'a str> for EncodedString {
242    type Error = Error;
243
244    fn try_from(s: &'a str) -> Result<Self> {
245        Self::new(s)
246    }
247}
248
249// Implement From<EncodedString> for String for easy unwrapping
250impl From<EncodedString> for String {
251    fn from(encoded: EncodedString) -> Self {
252        encoded.inner
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_new_valid() {
262        let encoded = EncodedString::new("zCn8eVZg").unwrap();
263        assert_eq!(encoded.base(), Base::Base58Btc);
264        assert_eq!(encoded.as_str(), "zCn8eVZg");
265    }
266
267    #[test]
268    fn test_new_empty_fails() {
269        let result = EncodedString::new("");
270        assert!(result.is_err());
271        assert!(matches!(result.unwrap_err(), Error::EmptyInput));
272    }
273
274    #[test]
275    fn test_new_invalid_base_fails() {
276        let result = EncodedString::new("xInvalid");
277        assert!(result.is_err());
278        assert!(matches!(result.unwrap_err(), Error::UnknownBase { .. }));
279    }
280
281    #[test]
282    fn test_decode() {
283        let encoded = EncodedString::new("zCn8eVZg").unwrap();
284        let decoded = encoded.decode().unwrap();
285        assert_eq!(decoded, b"hello");
286    }
287
288    #[test]
289    fn test_from_str() {
290        use core::str::FromStr;
291        let encoded = EncodedString::from_str("md29ybGQ").unwrap();
292        assert_eq!(encoded.base(), Base::Base64);
293    }
294
295    #[test]
296    fn test_try_from_string() {
297        use core::convert::TryFrom;
298        let encoded = EncodedString::try_from("f48656c6c6f".to_string()).unwrap();
299        assert_eq!(encoded.base(), Base::Base16Lower);
300    }
301
302    #[test]
303    fn test_try_from_str() {
304        use core::convert::TryFrom;
305        let encoded = EncodedString::try_from("BPFSXGIDNMFXGSIBB").unwrap();
306        assert_eq!(encoded.base(), Base::Base32Upper);
307    }
308
309    #[test]
310    fn test_into_inner() {
311        let encoded = EncodedString::new("zCn8eVZg").unwrap();
312        let inner = encoded.into_inner();
313        assert_eq!(inner, "zCn8eVZg");
314    }
315
316    #[test]
317    fn test_display() {
318        let encoded = EncodedString::new("zCn8eVZg").unwrap();
319        assert_eq!(format!("{}", encoded), "zCn8eVZg");
320    }
321
322    #[test]
323    fn test_clone() {
324        let encoded = EncodedString::new("zCn8eVZg").unwrap();
325        let cloned = encoded.clone();
326        assert_eq!(encoded, cloned);
327    }
328
329    #[test]
330    fn test_eq() {
331        let encoded1 = EncodedString::new("zCn8eVZg").unwrap();
332        let encoded2 = EncodedString::new("zCn8eVZg").unwrap();
333        let encoded3 = EncodedString::new("md29ybGQ").unwrap();
334
335        assert_eq!(encoded1, encoded2);
336        assert_ne!(encoded1, encoded3);
337    }
338
339    #[test]
340    fn test_as_ref() {
341        let encoded = EncodedString::new("zCn8eVZg").unwrap();
342        let s: &str = encoded.as_ref();
343        assert_eq!(s, "zCn8eVZg");
344    }
345}