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}