1use std::borrow::Cow;
4
5use hex_simd::AsciiCase;
6
7#[derive(Clone, PartialEq)]
8pub enum Encoder {
9 Hex,
10 Base64,
11 Windows1252,
15 Latin1,
18 Ascii,
21 Utf8,
22 Utf16le,
23 Utf16be,
24}
25
26const WINDOWS_1252_HIGH: [char; 32] = [
30 '\u{20AC}', '\u{0081}', '\u{201A}', '\u{0192}', '\u{201E}', '\u{2026}', '\u{2020}', '\u{2021}',
31 '\u{02C6}', '\u{2030}', '\u{0160}', '\u{2039}', '\u{0152}', '\u{008D}', '\u{017D}', '\u{008F}',
32 '\u{0090}', '\u{2018}', '\u{2019}', '\u{201C}', '\u{201D}', '\u{2022}', '\u{2013}', '\u{2014}',
33 '\u{02DC}', '\u{2122}', '\u{0161}', '\u{203A}', '\u{0153}', '\u{009D}', '\u{017E}', '\u{0178}',
34];
35
36fn windows_1252_to_string(bytes: &[u8]) -> String {
37 bytes
38 .iter()
39 .map(|b| match b {
40 0x80..=0x9F => WINDOWS_1252_HIGH[usize::from(b - 0x80)],
41 other => char::from(*other),
42 })
43 .collect()
44}
45
46fn string_to_windows_1252(string: &str) -> Vec<u8> {
47 string
48 .chars()
49 .map(|c| {
50 if let Some(index) = WINDOWS_1252_HIGH.iter().position(|high| *high == c) {
51 #[allow(clippy::cast_possible_truncation)]
52 return 0x80 + index as u8;
53 }
54 u8::try_from(u32::from(c)).unwrap_or(b'?')
55 })
56 .collect()
57}
58
59const NODE_ENCODING_MAP: phf::Map<&'static str, Encoder> = phf::phf_map! {
65 "buffer" => Encoder::Utf8,
66 "hex" => Encoder::Hex,
67 "base64" => Encoder::Base64,
68 "utf-8" => Encoder::Utf8,
69 "utf8" => Encoder::Utf8,
70 "ucs-2" => Encoder::Utf16le,
71 "ucs2" => Encoder::Utf16le,
72 "utf-16le" => Encoder::Utf16le,
73 "utf16le" => Encoder::Utf16le,
74 "latin1" => Encoder::Latin1,
75 "binary" => Encoder::Latin1,
76 "ascii" => Encoder::Ascii,
77};
78
79const ENCODING_MAP: phf::Map<&'static str, Encoder> = phf::phf_map! {
81 "ascii" => Encoder::Windows1252,
82 "latin1" => Encoder::Windows1252,
83 "buffer" => Encoder::Utf8,
84 "hex" => Encoder::Hex,
85 "base64" => Encoder::Base64,
86 "unicode-1-1-utf-8" => Encoder::Utf8,
87 "unicode11utf8" => Encoder::Utf8,
88 "unicode20utf8" => Encoder::Utf8,
89 "utf-8" => Encoder::Utf8,
90 "utf8" => Encoder::Utf8,
91 "x-unicode20utf8" => Encoder::Utf8,
92 "csunicode" => Encoder::Utf16le,
93 "iso-10646-ucs-2" => Encoder::Utf16le,
94 "ucs-2" => Encoder::Utf16le,
95 "ucs2" => Encoder::Utf16le,
96 "unicode" => Encoder::Utf16le,
97 "unicodefeff" => Encoder::Utf16le,
98 "utf-16" => Encoder::Utf16le,
99 "utf-16le" => Encoder::Utf16le,
100 "utf16le" => Encoder::Utf16le,
101 "unicodefffe" => Encoder::Utf16be,
102 "utf-16be" => Encoder::Utf16be,
103 "ansi_x3.4-1968" => Encoder::Windows1252,
104 "cp1252" => Encoder::Windows1252,
105 "cp819" => Encoder::Windows1252,
106 "csisolatin1" => Encoder::Windows1252,
107 "ibm819" => Encoder::Windows1252,
108 "iso-8859-1" => Encoder::Windows1252,
109 "iso-ir-100" => Encoder::Windows1252,
110 "iso8859-1" => Encoder::Windows1252,
111 "iso88591" => Encoder::Windows1252,
112 "iso_8859-1" => Encoder::Windows1252,
113 "iso_8859-1:1987" => Encoder::Windows1252,
114 "l1" => Encoder::Windows1252,
115 "us-ascii" => Encoder::Windows1252,
116 "windows-1252" => Encoder::Windows1252,
117 "x-cp1252" => Encoder::Windows1252,
118};
119
120impl Encoder {
121 pub fn from_optional_str(encoding: Option<&str>) -> Result<Self, String> {
122 match encoding {
123 Some(label) if !label.is_empty() => Self::from_str(label),
124 _ => Ok(Self::Utf8),
125 }
126 }
127
128 #[allow(clippy::should_implement_trait)]
130 pub fn from_str(encoding: &str) -> Result<Self, String> {
131 NODE_ENCODING_MAP
132 .get(encoding.trim_ascii().to_ascii_lowercase().as_str())
133 .cloned()
134 .ok_or_else(|| ["The \"", encoding, "\" encoding is not supported"].concat())
135 }
136
137 pub fn from_web_label(label: &str) -> Result<Self, String> {
139 ENCODING_MAP
140 .get(label.trim_ascii().to_ascii_lowercase().as_str())
141 .cloned()
142 .ok_or_else(|| ["The \"", label, "\" encoding is not supported"].concat())
143 }
144
145 pub fn from_optional_web_label(label: Option<&str>) -> Result<Self, String> {
147 match label {
148 Some(label) if !label.is_empty() => Self::from_web_label(label),
149 _ => Ok(Self::Utf8),
150 }
151 }
152
153 pub fn encode_to_string(&self, bytes: &[u8], lossy: bool) -> Result<String, String> {
154 match self {
155 Self::Hex => Ok(bytes_to_hex_string(bytes)),
156 Self::Base64 => Ok(bytes_to_b64_string(bytes)),
157 Self::Utf8 => bytes_to_utf8_string(bytes, lossy),
158 Self::Windows1252 => Ok(windows_1252_to_string(bytes)),
159 Self::Latin1 => Ok(bytes.iter().map(|b| char::from(*b)).collect()),
160 Self::Ascii => Ok(bytes.iter().map(|b| char::from(b & 0x7F)).collect()),
161 Self::Utf16le => bytes_to_utf16_string(bytes, Endian::Little, lossy),
162 Self::Utf16be => bytes_to_utf16_string(bytes, Endian::Big, lossy),
163 }
164 }
165
166 #[allow(dead_code)]
167 pub fn encode(&self, bytes: &[u8]) -> Result<Vec<u8>, String> {
168 match self {
169 Self::Hex => Ok(bytes_to_hex(bytes)),
170 Self::Base64 => Ok(bytes_to_b64(bytes)),
171 Self::Utf8 | Self::Windows1252 | Self::Latin1 | Self::Ascii | Self::Utf16le | Self::Utf16be => {
172 Ok(bytes.to_vec())
173 },
174 }
175 }
176
177 pub fn decode<'a, T: Into<Cow<'a, [u8]>>>(&self, bytes: T) -> Result<Vec<u8>, String> {
178 match self {
179 Self::Hex => bytes_from_hex(bytes),
180 Self::Base64 => bytes_from_b64(bytes),
181 Self::Utf8 | Self::Windows1252 | Self::Latin1 | Self::Ascii | Self::Utf16le | Self::Utf16be => {
182 Ok(bytes.into().into())
183 },
184 }
185 }
186
187 pub fn decode_from_string(&self, string: String) -> Result<Vec<u8>, String> {
188 match self {
189 Self::Hex => bytes_from_hex(string.into_bytes()),
190 Self::Base64 => bytes_from_b64(string.into_bytes()),
191 Self::Utf8 => Ok(string.into_bytes()),
192 Self::Windows1252 => Ok(string_to_windows_1252(&string)),
193 #[allow(clippy::cast_possible_truncation)]
196 Self::Latin1 => Ok(string.chars().map(|c| u32::from(c) as u8).collect()),
197 #[allow(clippy::cast_possible_truncation)]
198 Self::Ascii => Ok(string.chars().map(|c| (u32::from(c) as u8) & 0x7F).collect()),
199 Self::Utf16le => Ok(string
200 .encode_utf16()
201 .flat_map(|utf16| utf16.to_le_bytes())
202 .collect::<Vec<u8>>()),
203 Self::Utf16be => Ok(string
204 .encode_utf16()
205 .flat_map(|utf16| utf16.to_be_bytes())
206 .collect::<Vec<u8>>()),
207 }
208 }
209
210 pub fn as_label(&self) -> &str {
211 match self {
212 Self::Hex => "hex",
213 Self::Base64 => "base64",
214 Self::Windows1252 => "windows-1252",
215 Self::Latin1 => "latin1",
216 Self::Ascii => "ascii",
217 Self::Utf8 => "utf-8",
218 Self::Utf16le => "utf-16le",
219 Self::Utf16be => "utf-16be",
220 }
221 }
222}
223
224pub fn bytes_to_hex(bytes: &[u8]) -> Vec<u8> {
225 hex_simd::encode_type(bytes, AsciiCase::Lower)
226}
227
228pub fn bytes_from_hex<'a, T: Into<Cow<'a, [u8]>>>(hex_bytes: T) -> Result<Vec<u8>, String> {
229 hex_simd::decode_to_vec(hex_bytes.into()).map_err(|err| err.to_string())
230}
231
232pub fn bytes_from_b64<'a, T: Into<Cow<'a, [u8]>>>(base64_bytes: T) -> Result<Vec<u8>, String> {
233 let bytes: Cow<'a, [u8]> = base64_bytes.into();
234
235 let url_safe_byte_positions: Vec<usize> = memchr::memchr2_iter(b'-', b'_', &bytes).collect();
237
238 if url_safe_byte_positions.is_empty() {
239 return base64_simd::forgiving_decode_to_vec(&bytes).map_err(|e| e.to_string());
240 }
241
242 let mut bytes = bytes.into_owned();
244 for pos in url_safe_byte_positions {
245 bytes[pos] = match bytes[pos] {
246 b'-' => b'+',
247 b'_' => b'/',
248 _ => unreachable!(),
249 };
250 }
251 base64_simd::forgiving_decode_to_vec(&bytes).map_err(|e| e.to_string())
252}
253
254pub fn bytes_from_b64_strict(bytes: &[u8]) -> Result<Vec<u8>, String> {
257 base64_simd::STANDARD
258 .decode_to_vec(bytes)
259 .map_err(|e| e.to_string())
260}
261
262pub fn bytes_to_b64_string(bytes: &[u8]) -> String {
263 base64_simd::STANDARD.encode_to_string(bytes)
264}
265
266pub fn bytes_to_b64_url_safe_string(bytes: &[u8]) -> String {
267 base64_simd::URL_SAFE_NO_PAD.encode_to_string(bytes)
268}
269
270pub fn bytes_from_b64_url_safe(bytes: &[u8]) -> Result<Vec<u8>, String> {
271 base64_simd::URL_SAFE_NO_PAD
272 .decode_to_vec(bytes)
273 .map_err(|e| e.to_string())
274}
275
276pub fn bytes_to_b64(bytes: &[u8]) -> Vec<u8> {
277 base64_simd::STANDARD.encode_type(bytes)
278}
279
280pub fn bytes_to_hex_string(bytes: &[u8]) -> String {
281 hex_simd::encode_to_string(bytes, AsciiCase::Lower)
282}
283
284pub fn bytes_to_utf8_string(bytes: &[u8], lossy: bool) -> Result<String, String> {
285 if lossy {
286 Ok(String::from_utf8_lossy(bytes).to_string())
287 } else {
288 String::from_utf8(bytes.to_vec()).map_err(|e| e.to_string())
289 }
290}
291
292#[derive(Clone, Copy)]
293pub enum Endian {
294 Little,
295 Big,
296}
297
298pub fn bytes_to_utf16_string(bytes: &[u8], endian: Endian, lossy: bool) -> Result<String, String> {
299 if !lossy && !bytes.len().is_multiple_of(2) {
300 return Err("Input byte slice length must be even".to_string());
301 }
302
303 let data16: Vec<u16> = match endian {
304 Endian::Little => bytes
305 .as_chunks::<2>()
306 .0
307 .iter()
308 .copied()
309 .map(u16::from_le_bytes)
310 .collect(),
311 Endian::Big => bytes
312 .as_chunks::<2>()
313 .0
314 .iter()
315 .copied()
316 .map(u16::from_be_bytes)
317 .collect(),
318 };
319
320 let mut result = if lossy {
321 String::from_utf16_lossy(&data16)
322 } else {
323 String::from_utf16(&data16).map_err(|e| e.to_string())?
324 };
325
326 if lossy && !bytes.len().is_multiple_of(2) {
328 result.push('\u{FFFD}');
329 }
330
331 Ok(result)
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn b64_strict_matches_smithy_semantics() {
340 assert_eq!(bytes_from_b64_strict(b"SGVsbG8=").unwrap(), b"Hello");
342 assert!(bytes_from_b64_strict(b"-_8=").is_err());
344 assert!(bytes_from_b64_strict(b"SGVs bG8=").is_err());
345 assert!(bytes_from_b64_strict(b"SGVsbG8").is_err());
346 }
347}