debt64/
root.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Debt64
5
6Copyright (C) 2018-2019, 2021-2024  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2018-2019".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Root
28
29use {
30    alloc::{
31        string::String,
32        vec::Vec,
33    },
34    crate::Result,
35};
36
37mod encoder;
38mod decoder;
39
40/// # Base62 digits
41const BASE62_DIGITS: [char; 62] = [
42    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
43    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
44    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
45];
46
47/// # Pad char
48const PAD_CHAR: char = '=';
49
50/// # Last chars (#62 and #63)
51#[derive(Debug, Eq, PartialEq)]
52struct LastChars {
53
54    /// # The #62 char
55    first: char,
56
57    /// # The #63 char
58    last: char,
59
60}
61
62/// # Debt64
63///
64/// Not all variants are supported. Please follow the wiki article for details.
65#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
66pub enum Debt64 {
67
68    /// # Standard
69    ///
70    /// | Character #62 | Character #63 | Padding | Fixed line length | Max. line length | Line separators | Characters outside alphabet
71    /// | ------------- | ------------- | ------- | ----------------- | ---------------- | --------------- | ---------------------------
72    /// | `+`           | `/`           | `=`     | No                | None             | None            | Forbidden
73    Standard,
74
75    /// # IMAP mailbox names
76    ///
77    /// | Character #62 | Character #63 | Padding | Fixed line length | Max. line length | Line separators | Characters outside alphabet
78    /// | ------------- | ------------- | ------- | ----------------- | ---------------- | --------------- | ---------------------------
79    /// | `+`           | `,`           | None    | No                | None             | None            | Forbidden
80    IMAP,
81
82    /// # For MIME
83    ///
84    /// | Character #62 | Character #63 | Padding | Fixed line length | Max. line length | Line separators | Characters outside alphabet
85    /// | ------------- | ------------- | ------- | ----------------- | ---------------- | --------------- | ---------------------------
86    /// | `+`           | `/`           | `=`     | No                | `76`             | `\r\n`          | Accepted (discarded)
87    ///
88    /// ## Notes
89    ///
90    /// - Encoding produces above line separators.
91    /// - Decoding accepts above line separators and also just a single line feed `\n`.
92    /// - Decoding does _**not**_ allow consecutive line separators. For instance `\r\n\r\n` are not allowed.
93    MIME,
94
95    /// # URLs and filenames
96    ///
97    /// | Character #62 | Character #63 | Padding        | Fixed line length | Max. line length | Line separators | Characters outside alphabet
98    /// | ------------- | ------------- | -------------- | ----------------- | ---------------- | --------------- | ---------------------------
99    /// | `-`           | `_`           | `=` (optional) | No                | Optional         | None            | Forbidden
100    ///
101    /// ## Notes
102    ///
103    /// - This implementation has no limit on max line length.
104    /// - Encoding doesn't produce pad characters.
105    URL,
106
107    /// # URLs and filenames in Freenet
108    ///
109    /// | Character #62 | Character #63 | Padding        | Fixed line length | Max. line length | Line separators | Characters outside alphabet
110    /// | ------------- | ------------- | -------------- | ----------------- | ---------------- | --------------- | ---------------------------
111    /// | `~`           | `-`           | `=`            | No                | Optional         | None            | Forbidden
112    ///
113    /// ## Notes
114    ///
115    /// - This implementation has no limit on max line length.
116    ///
117    /// _Freenet: <https://en.wikipedia.org/wiki/Freenet>_
118    FreenetURL,
119
120}
121
122impl Debt64 {
123
124    const MIME_LINE_SEPARATORS: [char; 2] = ['\r', '\n'];
125
126    /// # Gets last characters (#62 and #63)
127    const fn last_chars(&self) -> &LastChars {
128        match self {
129            Self::Standard => &LastChars { first: '+', last: '/' },
130            Self::IMAP => &LastChars { first: '+', last: ',' },
131            Self::MIME => &LastChars { first: '+', last: '/' },
132            Self::URL => &LastChars { first: '-', last: '_' },
133            Self::FreenetURL => &LastChars { first: '~', last: '-' },
134        }
135    }
136
137    /// # Gets line separators
138    const fn line_separators(&self) -> Option<&[char]> {
139        match self {
140            Self::Standard => None,
141            Self::IMAP => None,
142            Self::MIME => Some(&Self::MIME_LINE_SEPARATORS),
143            Self::URL => None,
144            Self::FreenetURL => None,
145        }
146    }
147
148    /// # Gets max line length
149    const fn max_line_len(&self) -> Option<usize> {
150        match self {
151            Self::Standard => None,
152            Self::IMAP => None,
153            Self::MIME => Some(76),
154            Self::URL => None,
155            Self::FreenetURL => None,
156        }
157    }
158
159    /// # Checks if pad character is a must
160    const fn must_use_pad(&self) -> bool {
161        match self {
162            Self::Standard => true,
163            Self::IMAP => false,
164            Self::MIME => true,
165            Self::URL => false,
166            Self::FreenetURL => true,
167        }
168    }
169
170    /// # Checks if pad character can be used
171    const fn can_use_pad(&self) -> bool {
172        match self {
173            Self::Standard => true,
174            Self::IMAP => false,
175            Self::MIME => true,
176            Self::URL => true,
177            Self::FreenetURL => true,
178        }
179    }
180
181    /// # Gets character at given index
182    fn get_char(index: usize, last_chars: &LastChars) -> &char {
183        match index {
184            0..=61 => &BASE62_DIGITS[index],
185            62 => &last_chars.first,
186            _ => &last_chars.last,
187        }
188    }
189
190    /// # Checks to see if invalid characters are allowed
191    const fn allows_invalid_chars(&self) -> bool {
192        match self {
193            Self::Standard => false,
194            Self::IMAP => false,
195            Self::MIME => true,
196            Self::URL => false,
197            Self::FreenetURL => false,
198        }
199    }
200
201    /// # Encodes
202    pub fn encode<B>(&self, bytes: B) -> String where B: AsRef<[u8]> {
203        encoder::encode(bytes, self)
204    }
205
206    /// # Decodes
207    pub fn decode<B>(&self, bytes: B) -> Result<Vec<u8>> where B: AsRef<[u8]> {
208        decoder::decode(bytes, self)
209    }
210
211    /// # _Estimates_ encoding capacity
212    pub fn estimate_encoding_capacity<B>(&self, bytes: B) -> usize where B: AsRef<[u8]> {
213        let result = bytes.as_ref().len();
214        if result == 0 {
215            return 0;
216        }
217
218        let result = (result / 3).saturating_mul(4).saturating_add(match result % 3 {
219            0 => 0,
220            other => if self.must_use_pad() { 4 } else { other + 1 },
221        });
222        match (self.max_line_len().as_ref(), self.line_separators().as_ref()) {
223            (Some(max_line_len), Some(line_separators)) => result.saturating_add(
224                (result / max_line_len).saturating_sub(if result % max_line_len == 0 { 1 } else { 0 }).saturating_mul(line_separators.len())
225            ),
226            _ => result,
227        }
228    }
229
230    /// # _Estimates_ decoding capacity
231    pub fn estimate_decoding_capacity<B>(&self, bytes: B) -> usize where B: AsRef<[u8]> {
232        let result = bytes.as_ref().len();
233        if result == 0 {
234            return 0;
235        }
236
237        let line_separators = match (self.max_line_len().as_ref(), self.line_separators().as_ref()) {
238            (Some(max_line_len), Some(line_separators)) => {
239                let line_len = max_line_len.saturating_add(line_separators.len());
240                (result / line_len).saturating_mul(line_separators.len())
241            },
242            _ => 0,
243        };
244
245        let result = result.saturating_sub(line_separators);
246        let result = (result / 4).saturating_mul(3).saturating_add(match result % 4 {
247            0 | 1 => 0,
248            2 => 1,
249            _ => 2,
250        });
251
252        result
253    }
254
255}
256
257#[test]
258fn test_base62_digits() {
259    for (i, b) in (b'A'..b'Z').enumerate() {
260        assert_eq!(b as char, BASE62_DIGITS[i]);
261    }
262    for (i, b) in (b'a'..b'z').enumerate() {
263        assert_eq!(b as char, BASE62_DIGITS[i + 26]);
264    }
265    for (i, b) in (b'0'..b'9').enumerate() {
266        assert_eq!(b as char, BASE62_DIGITS[i + 52]);
267    }
268}
269
270#[test]
271fn test_last_chars() {
272    assert_eq!(Debt64::Standard.last_chars(), &LastChars { first: '+', last: '/' });
273    assert_eq!(Debt64::IMAP.last_chars(), &LastChars { first: '+', last: ',' });
274    assert_eq!(Debt64::MIME.last_chars(), &LastChars { first: '+', last: '/' });
275    assert_eq!(Debt64::URL.last_chars(), &LastChars { first: '-', last: '_' });
276    assert_eq!(Debt64::FreenetURL.last_chars(), &LastChars { first: '~', last: '-' });
277}
278
279#[test]
280fn test_line_separators() {
281    assert_eq!(Debt64::Standard.line_separators(), None);
282    assert_eq!(Debt64::IMAP.line_separators(), None);
283    assert_eq!(Debt64::MIME.line_separators().unwrap(), &['\r', '\n']);
284    assert_eq!(Debt64::URL.line_separators(), None);
285    assert_eq!(Debt64::FreenetURL.line_separators(), None);
286}
287
288#[test]
289fn test_max_line_len() {
290    assert_eq!(Debt64::Standard.max_line_len(), None);
291    assert_eq!(Debt64::IMAP.max_line_len(), None);
292    assert_eq!(Debt64::MIME.max_line_len(), Some(76));
293    assert_eq!(Debt64::URL.max_line_len(), None);
294    assert_eq!(Debt64::FreenetURL.max_line_len(), None);
295}
296
297#[test]
298fn test_pad_char() {
299    assert_eq!(Debt64::Standard.must_use_pad(), true);
300    assert_eq!(Debt64::Standard.can_use_pad(), true);
301
302    assert_eq!(Debt64::IMAP.must_use_pad(), false);
303    assert_eq!(Debt64::IMAP.can_use_pad(), false);
304
305    assert_eq!(Debt64::MIME.must_use_pad(), true);
306    assert_eq!(Debt64::MIME.can_use_pad(), true);
307
308    assert_eq!(Debt64::URL.must_use_pad(), false);
309    assert_eq!(Debt64::URL.can_use_pad(), true);
310
311    assert_eq!(Debt64::FreenetURL.must_use_pad(), true);
312    assert_eq!(Debt64::FreenetURL.can_use_pad(), true);
313}
314
315#[test]
316fn test_invalid_chars() {
317    assert_eq!(Debt64::Standard.allows_invalid_chars(), false);
318    assert_eq!(Debt64::IMAP.allows_invalid_chars(), false);
319    assert_eq!(Debt64::MIME.allows_invalid_chars(), true);
320    assert_eq!(Debt64::URL.allows_invalid_chars(), false);
321    assert_eq!(Debt64::FreenetURL.allows_invalid_chars(), false);
322}