Skip to main content

sashite_pin/
encode.rs

1//! Allocation-free string encoding of a PIN token.
2
3use crate::identifier::Identifier;
4use crate::state::State;
5
6/// The canonical string form of an [`Identifier`], stored inline.
7///
8/// A token occupies at most three bytes, so `EncodedPin` keeps them in a fixed
9/// buffer with no heap allocation. It is produced by [`Identifier::encode`] and
10/// dereferences to [`str`], so it can be used wherever a string slice is
11/// expected.
12///
13/// # Examples
14///
15/// ```
16/// # fn main() -> Result<(), sashite_pin::ParseError> {
17/// use sashite_pin::Identifier;
18///
19/// let enc = Identifier::parse("+K^")?.encode();
20/// assert_eq!(enc.as_str(), "+K^");
21/// assert_eq!(&*enc, "+K^"); // via Deref<Target = str>
22/// assert_eq!(enc.len(), 3); // str method reached through Deref
23/// assert_eq!(enc, "+K^"); // direct comparison via PartialEq<&str>
24/// assert_eq!("+K^", enc); // and the reverse direction
25/// # Ok(())
26/// # }
27/// ```
28#[derive(Clone, Copy)]
29pub struct EncodedPin {
30    buf: [u8; 3],
31    len: u8,
32}
33
34impl EncodedPin {
35    /// Encodes an identifier into its canonical token form.
36    #[must_use]
37    pub(crate) fn from_identifier(id: Identifier) -> Self {
38        let mut buf = [0u8; 3];
39        let mut len: u8 = 0;
40
41        // Optional state-modifier prefix.
42        if let Some(modifier) = state_modifier(id.state()) {
43            buf[usize::from(len)] = modifier;
44            len += 1;
45        }
46
47        // The cased abbreviation letter is always present.
48        buf[usize::from(len)] = id.letter().to_ascii(id.side());
49        len += 1;
50
51        // Optional terminal marker suffix.
52        if id.is_terminal() {
53            buf[usize::from(len)] = b'^';
54            len += 1;
55        }
56
57        Self { buf, len }
58    }
59
60    /// Returns the encoded token as a string slice.
61    #[must_use]
62    pub fn as_str(&self) -> &str {
63        let bytes = &self.buf[..usize::from(self.len)];
64        debug_assert!(bytes.is_ascii(), "EncodedPin must contain only ASCII bytes");
65        // ASCII is always valid UTF-8, so this conversion cannot fail; the empty
66        // fallback is unreachable and exists only to avoid `unsafe`.
67        core::str::from_utf8(bytes).unwrap_or("")
68    }
69}
70
71impl core::ops::Deref for EncodedPin {
72    type Target = str;
73
74    fn deref(&self) -> &str {
75        self.as_str()
76    }
77}
78
79impl AsRef<str> for EncodedPin {
80    fn as_ref(&self) -> &str {
81        self.as_str()
82    }
83}
84
85impl core::fmt::Display for EncodedPin {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        f.write_str(self.as_str())
88    }
89}
90
91impl core::fmt::Debug for EncodedPin {
92    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
93        write!(f, "EncodedPin({:?})", self.as_str())
94    }
95}
96
97impl PartialEq<str> for EncodedPin {
98    fn eq(&self, other: &str) -> bool {
99        self.as_str() == other
100    }
101}
102
103impl PartialEq<&str> for EncodedPin {
104    fn eq(&self, other: &&str) -> bool {
105        self.as_str() == *other
106    }
107}
108
109impl PartialEq<EncodedPin> for str {
110    fn eq(&self, other: &EncodedPin) -> bool {
111        self == other.as_str()
112    }
113}
114
115impl PartialEq<EncodedPin> for &str {
116    fn eq(&self, other: &EncodedPin) -> bool {
117        *self == other.as_str()
118    }
119}
120
121/// Encodes a state into its modifier byte. Inverse of the decoder in
122/// `parse.rs`.
123const fn state_modifier(state: State) -> Option<u8> {
124    match state {
125        State::Normal => None,
126        State::Enhanced => Some(b'+'),
127        State::Diminished => Some(b'-'),
128    }
129}