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}