1#[must_use]
5pub fn quote(s: &str) -> String {
6 format!("'{}'", s.replace('\'', "'\\''"))
7}
8
9#[must_use]
12pub fn base64(bytes: &[u8]) -> String {
13 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
14 for chunk in bytes.chunks(3) {
15 let [a, b, c] = [0, 1, 2].map(|i| chunk.get(i).copied().unwrap_or(0));
16 let sextets = [a >> 2, ((a & 0b11) << 4) | (b >> 4), ((b & 0b1111) << 2) | (c >> 6), c & 0b11_1111];
17 for (i, sextet) in sextets.into_iter().enumerate() {
19 out.push(if i <= chunk.len() { symbol(sextet) } else { '=' });
20 }
21 }
22 out
23}
24
25fn symbol(sextet: u8) -> char {
27 char::from(match sextet {
28 0..=25 => b'A' + sextet,
29 26..=51 => b'a' + (sextet - 26),
30 52..=61 => b'0' + (sextet - 52),
31 62 => b'+',
32 _ => b'/',
33 })
34}
35
36#[cfg(test)]
37mod tests {
38 use super::{base64, quote};
39
40 #[test]
41 fn base64_is_the_standard_one() {
42 for (raw, enc) in [
43 ("", ""),
44 ("a", "YQ=="),
45 ("ab", "YWI="),
46 ("abc", "YWJj"),
47 ("abcd", "YWJjZA=="),
48 ("abcde", "YWJjZGU="),
49 ("abcdef", "YWJjZGVm"),
50 ] {
51 assert_eq!(base64(raw.as_bytes()), enc);
52 }
53 assert_eq!(base64(&[0xff, 0xfe, 0x00]), "//4A");
54 }
55
56 #[test]
57 fn base64_uses_every_symbol_of_its_alphabet() {
58 let bytes = [
59 0x00, 0x10, 0x83, 0x10, 0x51, 0x87, 0x20, 0x92, 0x8b, 0x30, 0xd3, 0x8f, 0x41, 0x14, 0x93, 0x51, 0x55, 0x97, 0x61, 0x96, 0x9b, 0x71, 0xd7, 0x9f,
60 0x82, 0x18, 0xa3, 0x92, 0x59, 0xa7, 0xa2, 0x9a, 0xab, 0xb2, 0xdb, 0xaf, 0xc3, 0x1c, 0xb3, 0xd3, 0x5d, 0xb7, 0xe3, 0x9e, 0xbb, 0xf3, 0xdf, 0xbf,
61 ];
62 assert_eq!(base64(&bytes), "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
63 }
64
65 proptest::proptest! {
66 #[test]
67 fn base64_is_four_symbols_per_three_bytes_padded_at_the_end(bytes in proptest::collection::vec(proptest::prelude::any::<u8>(), 0..64)) {
68 let encoded = base64(&bytes);
69 proptest::prop_assert_eq!(encoded.len(), bytes.len().div_ceil(3) * 4);
70 let padding = encoded.chars().rev().take_while(|c| *c == '=').count();
71 proptest::prop_assert_eq!(padding, (3 - bytes.len() % 3) % 3);
72 proptest::prop_assert!(encoded.trim_end_matches('=').chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/'));
73 }
74 }
75
76 #[test]
77 fn plain_word_is_wrapped() {
78 assert_eq!(quote("abc"), "'abc'");
79 }
80
81 #[test]
82 fn empty_string_is_an_empty_word() {
83 assert_eq!(quote(""), "''");
84 }
85
86 #[test]
87 fn single_quotes_are_closed_escaped_and_reopened() {
88 assert_eq!(quote("it's"), "'it'\\''s'");
89 assert_eq!(quote("'"), "''\\'''");
90 }
91
92 #[test]
93 fn shell_metacharacters_stay_literal() {
94 assert_eq!(quote("$(rm -rf /) `x` \"y\" \\n"), "'$(rm -rf /) `x` \"y\" \\n'");
95 }
96}