netascii/
lib.rs

1//! This crate provides Iterator adapters for encoding ([`Netascii`]) and decoding ([`Bytes`])
2//! byte strings to and from netascii as according to [RFC 854](https://datatracker.ietf.org/doc/rfc854/).
3//! More concretely, these adapters deal with the escaping of carriage return (CR) characters.
4//!
5//! This crate is fully `no_std`-compatible and does not require `alloc`.
6
7#![cfg_attr(not(any(test, doctest)), no_std)]
8#![forbid(unsafe_code, unused_must_use)]
9
10use core::error::Error;
11use core::fmt::Display;
12
13/// An iterator adapter decoding the provided netascii source iterator.
14///
15/// Yields a [`CrError`] if an illegal carriage return is encountered.
16/// An illegal carriage return is one not followed by either a line feed or NUL (`b'\0'`).
17///
18/// After a [`CrError`], decoding continues at the next byte after the illegal CR.
19#[derive(Debug)]
20#[derive(Clone)]
21pub struct Bytes<I: Iterator<Item = u8>> {
22    netascii: core::iter::Peekable<I>,
23}
24
25impl<I: Iterator<Item = u8>> Bytes<I> {
26    pub fn from_netascii(netascii: impl IntoIterator<IntoIter = I>) -> Self {
27        Self {
28            netascii: netascii.into_iter().peekable(),
29        }
30    }
31}
32
33impl<I: Iterator<Item = u8>> Iterator for Bytes<I> {
34    type Item = Result<u8, CrError>;
35
36    fn next(&mut self) -> Option<Self::Item> {
37        Some(Ok(match self.netascii.next()? {
38            | b'\r' => match self.netascii.peek().copied() {
39                | Some(b'\n') => {
40                    self.netascii.next();
41                    b'\n'
42                }
43                | Some(b'\0') => {
44                    self.netascii.next();
45                    b'\r'
46                }
47                | _ => return Some(Err(CrError)),
48            },
49            | x => x,
50        }))
51    }
52
53    fn size_hint(&self) -> (usize, Option<usize>) {
54        let (netascii_lower, netascii_upper) = self.netascii.size_hint();
55        let lower = netascii_lower / 2;
56        let upper = netascii_upper;
57
58        (lower, upper)
59    }
60}
61
62/// An error indicating that an illegal carriage return character
63/// was encountered in the netascii source iterator.
64///
65/// See [`Bytes`] for more details on what constitutes an illegal CR.
66#[derive(Debug)]
67#[derive(Clone, Copy)]
68#[derive(PartialEq, Eq)]
69pub struct CrError;
70
71impl Display for CrError {
72    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
73        write!(
74            f,
75            "lone CR encountered (must be followed by either LF or NUL)"
76        )
77    }
78}
79
80impl Error for CrError {}
81
82/// An iterator adapter encoding the provided source iterator to netascii.
83///
84/// Inserts NUL (`b'\0'`) after every carriage return character not followed by a line feed.
85#[derive(Debug)]
86#[derive(Clone)]
87pub struct Netascii<I: Iterator> {
88    bytes: I,
89    next: Option<u8>,
90}
91
92impl<I: Iterator<Item = u8>> Netascii<I> {
93    pub fn from_bytes(bytes: impl IntoIterator<IntoIter = I>) -> Self {
94        Self {
95            bytes: bytes.into_iter(),
96            next: None,
97        }
98    }
99}
100
101impl<I: Iterator<Item = u8>> Iterator for Netascii<I> {
102    type Item = u8;
103
104    fn next(&mut self) -> Option<Self::Item> {
105        match self.next.take() {
106            | Some(c) => Some(c),
107            | None => Some(match self.bytes.next()? {
108                | b'\r' => {
109                    // we just took `next`
110                    debug_assert_eq!(self.next, None);
111                    self.next = Some(b'\0');
112                    b'\r'
113                }
114                | b'\n' => {
115                    // we just took `next`
116                    debug_assert_eq!(self.next, None);
117                    self.next = Some(b'\n');
118                    b'\r'
119                }
120                | c => c,
121            }),
122        }
123    }
124
125    fn size_hint(&self) -> (usize, Option<usize>) {
126        let (bytes_lower, bytes_upper) = self.bytes.size_hint();
127        let inserting = if self.next.is_some() { 1 } else { 0 };
128        let lower = bytes_lower.saturating_add(inserting);
129        let upper = bytes_upper
130            .map(|bytes_upper| bytes_upper.saturating_mul(2).saturating_add(inserting));
131        (lower, upper)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_inverse() {
141        let lorem = *br"
142        Lorem ipsum dolor sit amet,
143        consectetur adipiscing elit.
144        ";
145
146        let netascii = Netascii::from_bytes(lorem);
147        let bytes =
148            Bytes::from_netascii(netascii).collect::<Result<Vec<u8>, CrError>>().unwrap();
149
150        assert_eq!(&bytes, &lorem);
151    }
152
153    #[test]
154    fn test_cr_decode() {
155        let netascii = *b"lorem ipsum\r\n dolor sit\r\0 amet";
156        let expected = *b"lorem ipsum\n dolor sit\r amet";
157
158        let bytes =
159            Bytes::from_netascii(netascii).collect::<Result<Vec<u8>, CrError>>().unwrap();
160
161        assert_eq!(&bytes, &expected);
162    }
163
164    #[test]
165    fn test_cr_encode() {
166        let lorem = *b"lorem ipsum\n dolor sit\r amet";
167        let expected = *b"lorem ipsum\r\n dolor sit\r\0 amet";
168
169        let netascii = Netascii::from_bytes(lorem).collect::<Vec<u8>>();
170
171        assert_eq!(&netascii, &expected);
172    }
173
174    #[test]
175    fn test_no_same_repr_encode() {
176        let lorem = *b"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
177        let netascii = Netascii::from_bytes(lorem).collect::<Vec<u8>>();
178
179        assert_eq!(&netascii, &lorem);
180    }
181
182    #[test]
183    fn test_no_same_repr_decode() {
184        let netascii = *b"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
185        let decoded =
186            Bytes::from_netascii(netascii).collect::<Result<Vec<u8>, CrError>>().unwrap();
187
188        assert_eq!(&decoded, &netascii);
189    }
190
191    #[test]
192    fn test_illegal_cr() {
193        let netascii = *b"Lorem ipsum dolor sit\r amet, consectetur adipiscing elit.";
194        let decoded = Bytes::from_netascii(netascii).collect::<Result<Vec<u8>, _>>();
195        assert_eq!(Err(CrError), decoded);
196
197        let netascii = *b"Lorem ipsum dolor sit\r\r amet, consectetur adipiscing elit.";
198        let decoded = Bytes::from_netascii(netascii).collect::<Result<Vec<u8>, _>>();
199        assert_eq!(Err(CrError), decoded);
200
201        let netascii = *b"Lorem ipsum dolor sit\n\r amet, consectetur adipiscing elit.";
202        let decoded = Bytes::from_netascii(netascii).collect::<Result<Vec<u8>, _>>();
203        assert_eq!(Err(CrError), decoded);
204    }
205
206    #[test]
207    fn test_encode_size_hint() {
208        let lorem = *b"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
209        let netascii = Netascii::from_bytes(lorem);
210
211        assert!(netascii.size_hint().0 <= lorem.len());
212        assert!(netascii.size_hint().1 >= Some(lorem.len() * 2));
213    }
214
215    #[test]
216    fn test_decode_size_hint() {
217        let netascii = *b"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
218        let bytes = Bytes::from_netascii(netascii);
219
220        assert!(bytes.size_hint().0 <= netascii.len() / 2);
221        assert!(bytes.size_hint().1 >= Some(netascii.len()));
222    }
223}