Skip to main content

antimatter/capsule/
common.rs

1//! common functionality for all capsule versions
2
3use antimatter_api::models::tag_type_field::TagTypeField;
4use antimatter_api::models::{Tag, TagSetSpanTagsInner};
5use ciborium::de::from_reader;
6use serde::ser::{Error as SerdeError, Serializer};
7use serde::{Deserialize, Deserializer};
8use serde_repr::{Deserialize_repr, Serialize_repr};
9use serde_tuple::{Deserialize_tuple, Serialize_tuple};
10use std::collections::HashMap;
11use std::error::Error;
12use std::fmt;
13use std::io::Read;
14
15#[doc(hidden)]
16pub const VERSION_STRING: &str = "v0";
17#[doc(hidden)]
18pub const NONCE_SIZE: usize = 12; // The nonce size for AES-GCM is 12 bytes
19#[doc(hidden)]
20pub const NONCE_BLOCK_SIZE: usize = 6;
21#[doc(hidden)]
22pub const KEY_SIZE: usize = 32;
23#[doc(hidden)]
24pub const BUNDLE_MAGIC_BYTES: [u8; 8] = [249, 216, 132, 83, 144, 201, 2, 104];
25#[doc(hidden)]
26pub const BASE58_CHARSET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
27
28/// CapsuleError contains the possible errors that can be returned to the
29/// consumer from operations in this module.
30#[derive(Clone, Debug)]
31pub enum CapsuleError {
32    /// Generic is a generic error for all error types without a more
33    /// specific type.
34    Generic(String),
35    /// DEKNotFound is returned from the data encryption key for a
36    /// capsule cannot be found.
37    DEKNotFound(String),
38    /// DEKUnexpectedType is returned when the library finds a data
39    /// encryption key with an unknown or unexpected type.
40    DEKUnexpectedType(String),
41    /// DEKUnexpectedType is returned when the library finds a data
42    /// encryption key with an unexpected length
43    DEKWrongLength(String),
44    /// CBOREncodeFailed is returned when the library fails to encode
45    /// a value to CBOR.
46    CBOREncodeFailed(String),
47    /// CBOREncodeFailed is returned when the library fails to decode
48    /// what is expected to be a CBOR-encoded value.
49    CBORDecodeFailed(String),
50    /// EncryptionFailure is returned when an encryption operation fails.
51    EncryptionFailure(String),
52    /// DecryptionFailure is returned when a decryption operation fails.
53    DecryptionFailure(String),
54    /// BadMagic is returned when the library attempts to read a bundle
55    /// header but does not find the correct magic value in the header.
56    BadMagic(String),
57    /// UnsupportedVersion is returned when a capsule bundle header
58    /// contains an unsupported version.
59    UnsupportedVersion(String),
60    /// CapsuleAlreadySealed is returned when attempting to seal a
61    /// capsule that is already sealed.
62    CapsuleAlreadySealed(String),
63    /// StreamWriteFailure is returned when the library cannot write
64    /// to an I/O stream.
65    StreamWriteFailure(String),
66    /// StreamWriteFailure is returned when the library cannot read
67    /// from an I/O stream.
68    StreamReadFailure(String),
69    /// FileIOError is returned when the library encounters an I/O
70    /// error when attempting to read a capsule bundle.
71    FileIOError(String),
72    /// InsufficientPermissions is returned when attempting to read
73    /// a capsule with insufficient permissions.
74    InsufficientPermissions(String),
75    /// DRDecryptError is returned when attempting to invoke the DR
76    /// token decoder fails.
77    DRDecryptError(String),
78    /// CapsuleOpenError is returned when attempting to open a capsule
79    /// fails.
80    CapsuleOpenError(String),
81    /// CapsuleUpdateError is returned when the library encounters an
82    /// error when attempting to update a capsule.
83    CapsuleUpdateError(String),
84    EndOfRow,
85    EndOfCapsule,
86    CapsuleAccessDeniedByPolicy,
87    RowAccessDeniedByPolicy,
88}
89
90impl AsRef<str> for CapsuleError {
91    fn as_ref(&self) -> &str {
92        match self {
93            CapsuleError::Generic(msg) => msg,
94            CapsuleError::DEKNotFound(msg) => msg,
95            CapsuleError::DEKUnexpectedType(msg) => msg,
96            CapsuleError::DEKWrongLength(msg) => msg,
97            CapsuleError::CBOREncodeFailed(msg) => msg,
98            CapsuleError::CBORDecodeFailed(msg) => msg,
99            CapsuleError::EncryptionFailure(msg) => msg,
100            CapsuleError::DecryptionFailure(msg) => msg,
101            CapsuleError::BadMagic(msg) => msg,
102            CapsuleError::UnsupportedVersion(msg) => msg,
103            CapsuleError::CapsuleAlreadySealed(msg) => msg,
104            CapsuleError::StreamWriteFailure(msg) => msg,
105            CapsuleError::StreamReadFailure(msg) => msg,
106            CapsuleError::FileIOError(msg) => msg,
107            CapsuleError::InsufficientPermissions(msg) => msg,
108            CapsuleError::DRDecryptError(msg) => msg,
109            CapsuleError::CapsuleUpdateError(msg) => msg,
110            CapsuleError::CapsuleOpenError(msg) => msg,
111            CapsuleError::EndOfRow => "end of row",
112            CapsuleError::EndOfCapsule => "end of capsule",
113            CapsuleError::CapsuleAccessDeniedByPolicy => "capsule access denied by policy",
114            CapsuleError::RowAccessDeniedByPolicy => "row access denied by policy",
115        }
116    }
117}
118
119#[doc(hidden)]
120pub type PlaintextHeader = HashMap<String, Vec<u8>>;
121
122#[doc(hidden)]
123pub type EncryptedHeader = HashMap<String, Vec<u8>>;
124
125#[doc(hidden)]
126pub enum HeaderValue {
127    Str(String),
128    Bytes(Vec<u8>),
129}
130
131impl fmt::Display for CapsuleError {
132    // Implement the display method for each error type.
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            CapsuleError::Generic(msg) => {
136                write!(f, "{}", msg)
137            }
138            CapsuleError::DEKNotFound(msg) => {
139                write!(f, "DEK not found: {}", msg)
140            }
141            CapsuleError::DEKUnexpectedType(msg) => {
142                write!(f, "DEK has an unexpected type: {}", msg)
143            }
144            CapsuleError::DEKWrongLength(msg) => {
145                write!(f, "DEK has the wrong length: {}", msg)
146            }
147            CapsuleError::CBOREncodeFailed(msg) => {
148                write!(f, "failed to encode CBOR: {}", msg)
149            }
150            CapsuleError::CBORDecodeFailed(msg) => {
151                write!(f, "failed to decode CBOR: {}", msg)
152            }
153            CapsuleError::EncryptionFailure(msg) => {
154                write!(f, "failed to encrypt data: {}", msg)
155            }
156            CapsuleError::DecryptionFailure(msg) => {
157                write!(f, "failed to decrypt data: {}", msg)
158            }
159            CapsuleError::BadMagic(msg) => {
160                write!(f, "bad magic value detected: {}", msg)
161            }
162            CapsuleError::UnsupportedVersion(msg) => {
163                write!(f, "unsupported capsule version: {}", msg)
164            }
165            CapsuleError::CapsuleAlreadySealed(msg) => {
166                write!(f, "capsule is already sealed: {}", msg)
167            }
168            CapsuleError::StreamWriteFailure(msg) => {
169                write!(f, "failed to write to stream: {}", msg)
170            }
171            CapsuleError::StreamReadFailure(msg) => {
172                write!(f, "failed to read from stream: {}", msg)
173            }
174            CapsuleError::FileIOError(msg) => {
175                write!(f, "failed file IO operation: {}", msg)
176            }
177            CapsuleError::InsufficientPermissions(msg) => {
178                write!(f, "insufficient permissions: {}", msg)
179            }
180            CapsuleError::DRDecryptError(msg) => {
181                write!(f, "failed to decrypt the disaster recovery header: {}", msg)
182            }
183            CapsuleError::CapsuleOpenError(msg) => {
184                write!(f, "failed to open capsule: {}", msg)
185            }
186            CapsuleError::CapsuleUpdateError(msg) => {
187                write!(f, "failed to apply updates to the capsule: {}", msg)
188            }
189            CapsuleError::EndOfRow => {
190                write!(f, "end of row")
191            }
192            CapsuleError::EndOfCapsule => {
193                write!(f, "end of capsule")
194            }
195            CapsuleError::CapsuleAccessDeniedByPolicy => {
196                write!(f, "capsule access denied by policy")
197            }
198            CapsuleError::RowAccessDeniedByPolicy => {
199                write!(f, "row access denied by policy")
200            }
201        }
202    }
203}
204
205/// The definition of a column of data in a capsule as provided to the
206/// [`Session`]'s encapsulate function and returned from [`RowIterator`]
207/// after a capsule is opened.
208///
209/// [`Session`]: [`antimatter::session::session::Session`]
210/// [`RowIterator`]: [`antimatter::capsule::RowIterator`]
211#[derive(Clone, Serialize_tuple, Deserialize_tuple, Debug, PartialEq)]
212pub struct Column {
213    /// The name of the column. This is used when encapsulating with
214    /// the `subdomain_from` parameter specified.
215    pub name: String,
216    /// The tags to apply to the entire span of all cells in this column.
217    pub tags: Vec<CapsuleTag>,
218    /// Whether to skip data classification for all cells in this column.
219    pub skip_classification: bool,
220}
221
222#[doc(hidden)]
223#[derive(Clone, Serialize_tuple, Deserialize_tuple, Debug)]
224pub struct DataElement {
225    #[serde(with = "serde_bytes")]
226    pub data: Vec<u8>,
227    pub tags: Vec<SpanTag>,
228}
229
230/// A data cell in the input capsule table provided to the [`Session`]'s
231/// encapsulate function.
232///
233/// [`Session`]: [`antimatter::session::session::Session`]
234pub struct CellReader {
235    // TODO: get rid of this Box?
236    /// The cell data, as a Reader.
237    pub data: Box<dyn Read + Send>,
238    /// The span tags to attach to this cell, in addition to span tags
239    /// assigned by data classification if applicable.
240    pub tags: Vec<SpanTag>,
241}
242
243/// A data row in the input capsule table provided to the [`Session`]'s
244/// encapsulate function.
245///
246/// [`Session`]: [`antimatter::session::session::Session`]
247pub struct RowReader {
248    /// A list of cells contained in this row.
249    pub cells: Vec<CellReader>,
250    /// The tags to add with every cell in this row.
251    pub tags: Vec<CapsuleTag>,
252}
253
254impl CellReader {
255    /// Create a new [`CellReader`] for the given tags and data.
256    ///
257    /// **Arguments**
258    /// * `tags`: a set of tags to attach to the data, in addition to
259    ///      any tags assigned by data classification.
260    /// * `data`: the cell data, as a Reader.
261    ///
262    /// **Returns**
263    /// a new [`CellReader`]
264    pub fn new<R: Read + Send + 'static>(
265        tags: Vec<SpanTag>,
266        data: R,
267    ) -> Result<Self, CapsuleError> {
268        Ok(Self {
269            data: Box::new(data),
270            tags,
271        })
272    }
273
274    /// Return a copy of the remainder of the data in the [`CellReader`].
275    /// Note that this function replaces the original data so that it can
276    /// be read again.
277    ///
278    /// **Returns**
279    /// * `Vec<u8>`: a copy of the cell data.
280    pub fn copy_data(&mut self) -> Result<Vec<u8>, CapsuleError> {
281        let mut result: Vec<u8> = Vec::new();
282        self.data
283            .read_to_end(&mut result)
284            .map_err(|e| CapsuleError::Generic(format!("reading cell data: {}", e)))?;
285        let _ = std::mem::replace(
286            &mut self.data,
287            Box::new(std::io::Cursor::new(result.clone())),
288        );
289        Ok(result)
290    }
291}
292
293impl Read for CellReader {
294    fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
295        self.data.read(&mut buf[..])
296    }
297}
298
299#[doc(hidden)]
300#[derive(Debug, Clone, Serialize_tuple, Deserialize_tuple)]
301pub struct FileHeader {
302    pub magic: [u8; BUNDLE_MAGIC_BYTES.len()],
303    pub version: u8,
304}
305
306impl FileHeader {
307    pub fn new(version: u8) -> Self {
308        FileHeader {
309            magic: BUNDLE_MAGIC_BYTES,
310            version,
311        }
312    }
313
314    pub fn from_reader<R: Read>(r: R) -> Result<Self, CapsuleError> {
315        from_reader::<FileHeader, R>(r)
316            .map_err(|e| CapsuleError::Generic(format!("parsing FileHeader: {}", e)))
317    }
318
319    pub fn is_capsule_bytes(content: &[u8]) -> bool {
320        let header = from_reader::<FileHeader, &[u8]>(content);
321        match header.is_ok() {
322            true => header.unwrap().magic == BUNDLE_MAGIC_BYTES,
323            false => false,
324        }
325    }
326
327    /// is_capsule returns Ok((_, true)) if the argument reader r begins with
328    /// a valid capsule header, and Ok((_, false)) otherwise. If the argument
329    /// r cannot be read, then it returns Err(e). In the Ok case, the first
330    /// element of the returned tuple is a reader comprising the header bytes
331    /// followed by the remainder of the capsule which can be treated as
332    /// equal to the original argument reader r.
333    pub fn is_capsule<R: Read + 'static>(
334        mut r: R,
335    ) -> Result<(Box<dyn Read + 'static>, bool), CapsuleError> {
336        // The way CBOR encodes the header has a lot of nuances, but the
337        // header will be exactly 18 bytes so long as (a) the magic does
338        // not change; and (b) the capsule version is less than 24.
339        let len = 18;
340        let mut handle = r.by_ref().take(len as u64);
341        let mut header_bytes: Vec<u8> = Vec::new();
342
343        let n = handle
344            .read_to_end(&mut header_bytes)
345            .map_err(|e| CapsuleError::FileIOError(format!("reading capsule file: {}", e)))?;
346
347        if n < len {
348            // the reader doesn't have enough bytes to make up the header
349            return Ok((Box::new(std::io::Cursor::new(header_bytes)), false));
350        }
351
352        Ok((
353            Box::new(std::io::Cursor::new(header_bytes.clone()).chain(r)),
354            Self::is_capsule_bytes(&header_bytes),
355        ))
356    }
357}
358
359#[doc(hidden)]
360#[derive(Serialize_tuple, Deserialize_tuple, Clone)]
361pub struct BundleHeaderV2 {
362    // domain_id is the authorized domain that created the bundle.
363    #[serde(
364        serialize_with = "serialize_domain_id",
365        deserialize_with = "deserialize_domain_id"
366    )]
367    pub domain_id: String,
368    pub created: i64,
369    pub is_bundle: bool,
370}
371
372impl BundleHeaderV2 {
373    pub fn from_reader<R>(input: &mut R) -> Result<Self, CapsuleError>
374    where
375        R: Read,
376    {
377        ciborium::from_reader(input)
378            .map_err(|e| CapsuleError::Generic(format!("deserializing bundle header: {}", e)))
379    }
380}
381
382#[doc(hidden)]
383#[derive(Serialize_tuple, Deserialize_tuple, Clone)]
384pub struct BundleHeaderV3 {
385    // domain_id is the authorized domain that created the bundle.
386    #[serde(
387        serialize_with = "serialize_domain_id",
388        deserialize_with = "deserialize_domain_id"
389    )]
390    pub domain_id: String,
391    pub created: i64,
392    pub is_bundle: bool,
393}
394
395impl BundleHeaderV3 {
396    pub fn from_reader<R>(input: &mut R) -> Result<Self, CapsuleError>
397    where
398        R: Read,
399    {
400        ciborium::from_reader(input)
401            .map_err(|e| CapsuleError::Generic(format!("deserializing bundle header: {}", e)))
402    }
403}
404
405#[doc(hidden)]
406#[derive(Serialize_tuple, Deserialize_tuple, Clone)]
407pub struct CapsuleHeader {
408    #[serde(with = "serde_bytes")]
409    pub encrypted_dek: Vec<u8>,
410    pub key_id: u64,
411    #[serde(
412        serialize_with = "serialize_domain_id",
413        deserialize_with = "deserialize_domain_id"
414    )]
415    pub domain_id: String,
416    #[serde(
417        serialize_with = "serialize_capsule_id",
418        deserialize_with = "deserialize_capsule_id"
419    )]
420    pub capsule_id: String,
421    #[serde(skip_serializing_if = "Option::is_none", with = "serde_bytes", default)]
422    pub disaster_recovery_token: Option<Vec<u8>>,
423}
424
425impl CapsuleHeader {
426    pub fn from_reader<R>(input: &mut R) -> Result<Self, CapsuleError>
427    where
428        R: Read,
429    {
430        ciborium::from_reader(input)
431            .map_err(|e| CapsuleError::Generic(format!("deserializing capsule header: {}", e)))
432    }
433}
434
435#[doc(hidden)]
436#[derive(Serialize_tuple, Deserialize_tuple, Clone, PartialEq)]
437pub struct HookInfo {
438    pub name: String,
439    pub version: String,
440}
441
442/// The available supported tag types.
443#[derive(Eq, Hash, Clone, Serialize_repr, Deserialize_repr, Debug, PartialEq, PartialOrd)]
444#[repr(u8)]
445pub enum TagType {
446    /// A tag that consists of a key without a value.
447    Unary,
448    /// A tag that consists of a key with a string value.
449    Str,
450    /// A tag that consists of a key with a numeric value.
451    Number,
452    /// A tag that consists of a key with a boolean value.
453    Boolean,
454    /// A tag that consists of a key with a datetime value.
455    Date,
456}
457
458// convert from and to TagTypeField to TagType
459impl From<TagTypeField> for TagType {
460    fn from(tag_type: TagTypeField) -> Self {
461        match tag_type {
462            TagTypeField::String => TagType::Str,
463            TagTypeField::Number => TagType::Number,
464            TagTypeField::Boolean => TagType::Boolean,
465            TagTypeField::Date => TagType::Date,
466            TagTypeField::Unary => TagType::Unary,
467        }
468    }
469}
470
471impl From<TagType> for TagTypeField {
472    fn from(tag_type: TagType) -> Self {
473        match tag_type {
474            TagType::Str => TagTypeField::String,
475            TagType::Number => TagTypeField::Number,
476            TagType::Boolean => TagTypeField::Boolean,
477            TagType::Date => TagTypeField::Date,
478            TagType::Unary => TagTypeField::Unary,
479        }
480    }
481}
482
483/// A tag that applies to an entire capsule, either specified by the user
484/// at capsule creation or computed by a classification hook.
485#[derive(Clone, Serialize_tuple, Deserialize_tuple, Debug, Eq, Hash)]
486pub struct CapsuleTag {
487    /// The name of the tag, following a URI-based FQDN naming scheme.
488    pub name: String,
489    /// The type of the tag.
490    pub tag_type: TagType,
491    /// The tag value, if applicable. An empty string for Unary tags.
492    pub value: String,
493    /// The name of the hook that created the tag. Can be an empty
494    /// string if the tag is user-specified.
495    pub source: String,
496    /// The version of the hook that created the tag.
497    pub hook_version: (i32, i32, i32),
498}
499
500impl CapsuleTag {
501    /// Helper function to convert an [`Tag`] to a [`CapsuleTag`].
502    ///
503    /// **Arguments**
504    /// * `tag` - The [`Tag`] object to be converted.
505    ///
506    /// **Returns**
507    /// a new [`CapsuleTag`].
508    pub fn from_tag(tag: &Tag) -> Result<CapsuleTag, CapsuleError> {
509        let tuple = convert_to_tuple(&tag.hook_version.clone().unwrap())?;
510        Ok(CapsuleTag {
511            name: tag.name.clone(),
512            tag_type: TagType::from(tag.r#type),
513            value: tag.value.clone(),
514            source: tag.source.clone(),
515            hook_version: tuple,
516        })
517    }
518}
519
520impl PartialEq for CapsuleTag {
521    fn eq(&self, other: &Self) -> bool {
522        self.name == other.name && self.tag_type == other.tag_type && self.value == other.value
523    }
524}
525
526impl From<CapsuleTag> for Tag {
527    fn from(capsule_tag: CapsuleTag) -> Self {
528        Self {
529            name: capsule_tag.name.clone(),
530            r#type: match capsule_tag.tag_type {
531                TagType::Str => TagTypeField::String,
532                TagType::Number => TagTypeField::Number,
533                TagType::Boolean => TagTypeField::Boolean,
534                TagType::Date => TagTypeField::Date,
535                TagType::Unary => TagTypeField::Unary,
536            },
537            value: capsule_tag.value.clone(),
538            source: capsule_tag.source.clone(),
539            hook_version: Some(format!(
540                "{}.{}.{}",
541                capsule_tag.hook_version.0, capsule_tag.hook_version.1, capsule_tag.hook_version.2
542            )),
543        }
544    }
545}
546
547/// A tag that applies to a span within a data cell of a capsule.
548#[derive(Clone, Serialize_tuple, Deserialize_tuple, Debug, PartialEq, Eq)]
549pub struct SpanTag {
550    /// The tag itself.
551    pub tag: CapsuleTag,
552    /// The index of the beginning of the tagged data.
553    pub start: usize,
554    /// The index of the end of the tagged data (exclusive).
555    pub end: usize,
556}
557
558impl SpanTag {
559    /// Helper function to convert an [`TagSetSpanTagsInner]` to a [`SpanTag`].
560    ///
561    /// **Arguments**
562    /// * `inner` - The [`TagSetSpanTagsInner`] object to be converted.
563    ///
564    /// **Returns**
565    /// A new [`SpanTag`].
566    pub fn from_api_span_inner(inner: &TagSetSpanTagsInner) -> Result<Vec<SpanTag>, CapsuleError> {
567        let mut output: Vec<SpanTag> = Vec::new();
568        for tag in &inner.tags {
569            output.push(SpanTag {
570                tag: CapsuleTag::from_tag(tag)?,
571                start: inner.start as usize,
572                end: inner.end as usize,
573            });
574        }
575        Ok(output)
576    }
577}
578
579impl From<SpanTag> for TagSetSpanTagsInner {
580    fn from(span_tag: SpanTag) -> Self {
581        Self {
582            start: span_tag.start as i64,
583            end: span_tag.end as i64,
584            tags: vec![span_tag.tag.into()],
585        }
586    }
587}
588
589// These are the current decisions we support from the policy engine
590#[doc(hidden)]
591#[derive(PartialEq, Debug, Copy, Clone)]
592pub enum PolicyDecision {
593    Allow,
594    Redact,
595    Tokenize,
596    DenyRecord,
597    DenyCapsule,
598    NoMatch,
599}
600
601// convert_to_tuple is a helper function to convert the hook version into a
602// format compatible with the capsule. Currently the tuple is consumed as a
603// dot-seperated string containing 3 element, we convert this to a tuple.
604fn convert_to_tuple(input: &str) -> Result<(i32, i32, i32), CapsuleError> {
605    let parts: Vec<&str> = input.split('.').collect();
606
607    if parts.len() != 3 {
608        return Err(CapsuleError::Generic(
609            "Input string does not contain exactly three parts".to_string(),
610        ));
611    }
612
613    let part1 = parts[0].parse::<i32>();
614    let part2 = parts[1].parse::<i32>();
615    let part3 = parts[2].parse::<i32>();
616
617    match (part1, part2, part3) {
618        (Ok(p1), Ok(p2), Ok(p3)) => Ok((p1, p2, p3)),
619        _ => Err(CapsuleError::Generic(
620            "Failed to parse one or more parts into an integer".to_string(),
621        )),
622    }
623}
624
625fn base58_to_packed_bytes(input: &str) -> Result<Vec<u8>, Box<dyn Error>> {
626    let bits: Vec<u8> = input
627        .chars()
628        .map(|c| {
629            BASE58_CHARSET
630                .find(c)
631                .map(|idx| idx as u8)
632                .ok_or_else(|| "Invalid base58 character".into())
633        })
634        .collect::<Result<Vec<u8>, Box<dyn Error>>>()?;
635
636    let mut bytes = Vec::new();
637    let mut accumulator = 0u16; // Holds up to 12 bits
638    let mut bits_in_accumulator = 0;
639
640    for bit_value in bits {
641        accumulator <<= 6;
642        accumulator |= bit_value as u16;
643        bits_in_accumulator += 6;
644
645        if bits_in_accumulator >= 8 {
646            bits_in_accumulator -= 8;
647            bytes.push((accumulator >> bits_in_accumulator) as u8);
648        }
649    }
650
651    // Handle any remaining bits
652    if bits_in_accumulator > 0 {
653        bytes.push((accumulator << (8 - bits_in_accumulator)) as u8);
654    }
655    Ok(bytes)
656}
657
658fn serialize_base58<S>(prefix: &str, input: &str, serializer: S) -> Result<S::Ok, S::Error>
659where
660    S: Serializer,
661{
662    let stripped = input.strip_prefix(prefix).ok_or_else(|| {
663        S::Error::custom(format!("invalid ID format (must begin with {})", prefix))
664    })?;
665    serializer.serialize_bytes(
666        &base58_to_packed_bytes(stripped)
667            .map_err(S::Error::custom)?
668            .to_vec(),
669    )
670}
671
672#[doc(hidden)]
673pub fn serialize_domain_id<S>(domain_id: &str, serializer: S) -> Result<S::Ok, S::Error>
674where
675    S: Serializer,
676{
677    serialize_base58("dm-", domain_id, serializer)
678}
679
680#[doc(hidden)]
681pub fn serialize_capsule_id<S>(capsule_id: &str, serializer: S) -> Result<S::Ok, S::Error>
682where
683    S: Serializer,
684{
685    serialize_base58("ca-", capsule_id, serializer)
686}
687
688fn unpack_base58_bytes(input: &[u8]) -> Result<String, Box<dyn Error>> {
689    let mut bits = Vec::new();
690    let mut accumulator = 0u16; // Holds up to 16 bits
691    let mut bits_in_accumulator = 0;
692
693    for &byte in input {
694        accumulator = (accumulator << 8) | (byte as u16);
695        bits_in_accumulator += 8;
696
697        while bits_in_accumulator >= 6 {
698            bits_in_accumulator -= 6;
699            let index = ((accumulator >> bits_in_accumulator) & 0x3F) as usize; // 0x3F (63) masks the lower 6 bits
700            bits.push(index);
701        }
702    }
703
704    if bits_in_accumulator > 0 {
705        let index = ((accumulator << (6 - bits_in_accumulator)) & 0x3F) as usize;
706        bits.push(index);
707    }
708
709    // Convert 6-bit values back to base58 characters
710    let result: String = bits
711        .iter()
712        .map(|&idx| BASE58_CHARSET.chars().nth(idx).ok_or("Invalid 6-bit value"))
713        .collect::<Result<String, &str>>()?;
714
715    Ok(result)
716}
717
718fn deserialize_base58<'de, D>(len: usize, prefix: &str, deserializer: D) -> Result<String, D::Error>
719where
720    D: Deserializer<'de>,
721{
722    let packed: Vec<u8> = Deserialize::deserialize(deserializer)?;
723    let suffix: String = unpack_base58_bytes(packed.as_slice())
724        .map_err(serde::de::Error::custom)?
725        .chars()
726        .take(len)
727        .collect();
728    Ok(format!("{}{}", prefix, suffix))
729}
730
731#[doc(hidden)]
732pub fn deserialize_domain_id<'de, D>(deserializer: D) -> Result<String, D::Error>
733where
734    D: Deserializer<'de>,
735{
736    deserialize_base58(11, "dm-", deserializer)
737}
738
739#[doc(hidden)]
740pub fn deserialize_capsule_id<'de, D>(deserializer: D) -> Result<String, D::Error>
741where
742    D: Deserializer<'de>,
743{
744    deserialize_base58(22, "ca-", deserializer)
745}