sise_atom/
lib.rs

1// Copyright 2019 Eduardo Sánchez Muñoz
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! This crate provides auxiliary functions used to encode and decode
9//! S-expression atom values.
10//!
11//! # Minimum Rust version
12//!
13//! The minimum Rust version required by this crate is 1.55.
14
15#![deny(
16    rust_2018_idioms,
17    trivial_casts,
18    trivial_numeric_casts,
19    unreachable_pub,
20    unused_must_use,
21    unused_qualifications
22)]
23#![forbid(unsafe_code)]
24#![no_std]
25
26extern crate alloc;
27
28use alloc::string::{String, ToString as _};
29use alloc::vec::Vec;
30use core::convert::TryFrom as _;
31use core::fmt::Write as _;
32
33#[cfg(test)]
34mod tests;
35
36// Make this crate only compile on Rust >=1.55
37// because previous versions have buggy float parsing.
38// Open range patterns (e.g., `0..`) have been stabilized
39// in Rust 1.55, so this will fail to compile on previous
40// versions.
41const _: bool = matches!(0, 0..);
42
43// Encode
44/// Encodes a boolean value
45///
46///
47/// Returns `"true"` if `value` is `true`, otherwise returns `"false"`.
48pub fn encode_bool(value: bool) -> &'static str {
49    if value {
50        "true"
51    } else {
52        "false"
53    }
54}
55
56/// Encodes a signed 8-bit integer
57#[inline]
58pub fn encode_i8(value: i8) -> String {
59    value.to_string()
60}
61
62/// Encodes a signed 16-bit integer
63#[inline]
64pub fn encode_i16(value: i16) -> String {
65    value.to_string()
66}
67
68/// Encodes a signed 32-bit integer
69#[inline]
70pub fn encode_i32(value: i32) -> String {
71    value.to_string()
72}
73
74/// Encodes a signed 64-bit integer
75#[inline]
76pub fn encode_i64(value: i64) -> String {
77    value.to_string()
78}
79
80/// Encodes a signed 128-bit integer
81#[inline]
82pub fn encode_i128(value: i128) -> String {
83    value.to_string()
84}
85
86/// Encodes an unsigned 8-bit integer
87#[inline]
88pub fn encode_u8(value: u8) -> String {
89    value.to_string()
90}
91
92/// Encodes an unsigned 16-bit integer
93#[inline]
94pub fn encode_u16(value: u16) -> String {
95    value.to_string()
96}
97
98/// Encodes an unsigned 32-bit integer
99#[inline]
100pub fn encode_u32(value: u32) -> String {
101    value.to_string()
102}
103
104/// Encodes an unsigned 64-bit integer
105#[inline]
106pub fn encode_u64(value: u64) -> String {
107    value.to_string()
108}
109
110/// Encodes an unsigned 128-bit integer
111#[inline]
112pub fn encode_u128(value: u128) -> String {
113    value.to_string()
114}
115
116fn reformat_float(s: String) -> String {
117    if s == "NaN" || s == "inf" || s == "-inf" {
118        return s;
119    }
120
121    const ZEROS_THRESHOLD: u32 = 9;
122
123    let mut result = String::new();
124    let s = if let Some(remaining) = s.strip_prefix('-') {
125        result.push('-');
126        remaining
127    } else {
128        s.as_str()
129    };
130
131    if let Some(mut remaining) = s.strip_prefix("0.") {
132        let mut exp_abs: u32 = 1;
133        while let Some(new_remaining) = remaining.strip_prefix('0') {
134            remaining = new_remaining;
135            exp_abs += 1;
136        }
137        if exp_abs > ZEROS_THRESHOLD {
138            result.push_str(&remaining[..1]);
139            if remaining.len() > 1 {
140                result.push('.');
141                result.push_str(&remaining[1..]);
142            } else {
143                result.push_str(".0");
144            }
145            result.push_str("e-");
146            write!(result, "{}", exp_abs).unwrap();
147        } else {
148            result.push_str(s);
149        }
150    } else {
151        let s = s.strip_suffix(".0").unwrap_or(s);
152        if s.contains('.') {
153            result.push_str(s);
154        } else {
155            let mut remaining = s;
156            let mut num_zeros: u32 = 0;
157            while let Some(new_remaining) = remaining.strip_suffix('0') {
158                remaining = new_remaining;
159                num_zeros += 1;
160            }
161            if num_zeros > ZEROS_THRESHOLD {
162                result.push_str(&remaining[..1]);
163                if remaining.len() > 1 {
164                    result.push('.');
165                    result.push_str(&remaining[1..]);
166                } else {
167                    result.push_str(".0");
168                }
169                result.push('e');
170                write!(result, "{}", num_zeros + (remaining.len() as u32 - 1)).unwrap();
171            } else {
172                result.push_str(s);
173                result.push_str(".0");
174            }
175        }
176    }
177
178    result
179}
180
181/// Encodes a 32-bit floating point number
182#[inline]
183pub fn encode_f32(value: f32) -> String {
184    reformat_float(value.to_string())
185}
186
187/// Encodes a 64-bit floating point number
188#[inline]
189pub fn encode_f64(value: f64) -> String {
190    reformat_float(value.to_string())
191}
192
193#[inline]
194fn nibble_to_hex(nibble: u8) -> char {
195    if nibble < 10 {
196        char::from(b'0' + nibble)
197    } else {
198        char::from(b'a' + nibble - 10)
199    }
200}
201
202pub fn encode_byte_string(string: &[u8]) -> String {
203    let mut result = String::with_capacity(string.len() + 2);
204    result.push('"');
205    for &chr in string {
206        #[allow(clippy::match_overlapping_arm)]
207        match chr {
208            b'\0' => result.push_str("\\0"),
209            b'\t' => result.push_str("\\t"),
210            b'\n' => result.push_str("\\n"),
211            b'\r' => result.push_str("\\r"),
212            b'"' => result.push_str("\\\""),
213            b'\\' => result.push_str("\\\\"),
214            0x20..=0x7E => result.push(char::from(chr)),
215            _ => {
216                result.push_str("\\x");
217                result.push(nibble_to_hex(chr >> 4));
218                result.push(nibble_to_hex(chr & 0xF));
219            }
220        }
221    }
222    result.push('"');
223    result
224}
225
226pub fn encode_ascii_string(string: &str) -> String {
227    let mut result = String::with_capacity(string.len() + 2);
228    result.push('"');
229    for &chr in string.as_bytes() {
230        #[allow(clippy::match_overlapping_arm)]
231        match chr {
232            b'\0' => result.push_str("\\0"),
233            b'\t' => result.push_str("\\t"),
234            b'\n' => result.push_str("\\n"),
235            b'\r' => result.push_str("\\r"),
236            b'"' => result.push_str("\\\""),
237            b'\\' => result.push_str("\\\\"),
238            0x20..=0x7E => result.push(char::from(chr)),
239            _ => {
240                assert!(chr <= 0x7F, "Invalid ASCII character");
241                result.push_str("\\x");
242                result.push(nibble_to_hex(chr >> 4));
243                result.push(nibble_to_hex(chr & 0xF));
244            }
245        }
246    }
247    result.push('"');
248    result
249}
250
251pub fn encode_utf8_string(string: &str) -> String {
252    let mut result = String::with_capacity(string.len() + 2);
253    result.push('"');
254    for chr in string.chars() {
255        match chr {
256            '\0' => result.push_str("\\0"),
257            '\t' => result.push_str("\\t"),
258            '\n' => result.push_str("\\n"),
259            '\r' => result.push_str("\\r"),
260            '"' => result.push_str("\\\""),
261            '\\' => result.push_str("\\\\"),
262            '\x20'..='\x7E' => result.push(chr),
263            _ => {
264                result.push_str("\\u{");
265                let code_beginning = result.len();
266                let mut remaining_digits = u32::from(chr);
267                loop {
268                    result.insert(
269                        code_beginning,
270                        nibble_to_hex((remaining_digits & 0xF) as u8),
271                    );
272                    remaining_digits >>= 4;
273                    if remaining_digits == 0 {
274                        break;
275                    }
276                }
277                result.push('}');
278            }
279        }
280    }
281    result.push('"');
282    result
283}
284
285// Decode
286/// Decodes a boolean value
287///
288/// Returns `Some(true)` if `atom` is `"true"`, `Some(false)`
289/// if `atom` is `"false"`, or `None` otherwise.
290#[inline]
291pub fn decode_bool(atom: &str) -> Option<bool> {
292    match atom {
293        "false" => Some(false),
294        "true" => Some(true),
295        _ => None,
296    }
297}
298
299/// Decodes a signed 8-bit integer
300#[inline]
301pub fn decode_i8(atom: &str) -> Option<i8> {
302    atom.parse().ok()
303}
304
305/// Decodes a signed 16-bit integer
306#[inline]
307pub fn decode_i16(atom: &str) -> Option<i16> {
308    atom.parse().ok()
309}
310
311/// Decodes a signed 32-bit integer
312#[inline]
313pub fn decode_i32(atom: &str) -> Option<i32> {
314    atom.parse().ok()
315}
316
317/// Decodes a signed 64-bit integer
318#[inline]
319pub fn decode_i64(atom: &str) -> Option<i64> {
320    atom.parse().ok()
321}
322
323/// Decodes a signed 128-bit integer
324#[inline]
325pub fn decode_i128(atom: &str) -> Option<i128> {
326    atom.parse().ok()
327}
328
329/// Decodes an unsigned 8-bit integer
330#[inline]
331pub fn decode_u8(atom: &str) -> Option<u8> {
332    atom.parse().ok()
333}
334
335/// Decodes an unsigned 16-bit integer
336#[inline]
337pub fn decode_u16(atom: &str) -> Option<u16> {
338    atom.parse().ok()
339}
340
341/// Decodes an unsigned 32-bit integer
342#[inline]
343pub fn decode_u32(atom: &str) -> Option<u32> {
344    atom.parse().ok()
345}
346
347/// Decodes an unsigned 64-bit integer
348#[inline]
349pub fn decode_u64(atom: &str) -> Option<u64> {
350    atom.parse().ok()
351}
352
353/// Decodes an unsigned 128-bit integer
354#[inline]
355pub fn decode_u128(atom: &str) -> Option<u128> {
356    atom.parse().ok()
357}
358
359/// Decodes a 32-bit floating point number
360#[inline]
361pub fn decode_f32(atom: &str) -> Option<f32> {
362    if atom == "+NaN" || atom == "-NaN" {
363        None
364    } else {
365        atom.parse().ok()
366    }
367}
368
369/// Decodes a 64-bit floating point number
370#[inline]
371pub fn decode_f64(atom: &str) -> Option<f64> {
372    if atom == "+NaN" || atom == "-NaN" {
373        None
374    } else {
375        atom.parse().ok()
376    }
377}
378
379#[inline]
380fn hex_digit_byte_to_u8(chr: u8) -> Option<u8> {
381    match chr {
382        b'0'..=b'9' => Some(chr - b'0'),
383        b'A'..=b'F' => Some(chr - b'A' + 10),
384        b'a'..=b'f' => Some(chr - b'a' + 10),
385        _ => None,
386    }
387}
388
389#[inline]
390fn hex_digit_char_to_u8(chr: char) -> Option<u8> {
391    match chr {
392        '0'..='9' => Some(chr as u8 - b'0'),
393        'A'..='F' => Some(chr as u8 - b'A' + 10),
394        'a'..='f' => Some(chr as u8 - b'a' + 10),
395        _ => None,
396    }
397}
398
399pub fn decode_byte_string(atom: &str) -> Option<Vec<u8>> {
400    let mut iter = atom.bytes();
401    if iter.next() != Some(b'"') {
402        return None;
403    }
404
405    let mut string = Vec::new();
406    loop {
407        match iter.next() {
408            None => return None,
409            Some(b'"') => {
410                if iter.next().is_some() {
411                    return None;
412                }
413                break;
414            }
415            Some(b'\\') => match iter.next() {
416                Some(b'0') => string.push(b'\0'),
417                Some(b't') => string.push(b'\t'),
418                Some(b'n') => string.push(b'\n'),
419                Some(b'r') => string.push(b'\r'),
420                Some(b'"') => string.push(b'\"'),
421                Some(b'\\') => string.push(b'\\'),
422                Some(b'x') => {
423                    let hex1 = hex_digit_byte_to_u8(iter.next()?)?;
424                    let hex2 = hex_digit_byte_to_u8(iter.next()?)?;
425                    string.push((hex1 << 4) | hex2);
426                }
427                Some(_) | None => return None,
428            },
429            Some(byte) => string.push(byte),
430        }
431    }
432
433    Some(string)
434}
435
436pub fn decode_ascii_string(atom: &str) -> Option<String> {
437    let mut iter = atom.bytes();
438    if iter.next() != Some(b'"') {
439        return None;
440    }
441
442    let mut string = String::new();
443    loop {
444        match iter.next() {
445            None => return None,
446            Some(b'"') => {
447                if iter.next().is_some() {
448                    return None;
449                }
450                break;
451            }
452            Some(b'\\') => match iter.next() {
453                Some(b'0') => string.push('\0'),
454                Some(b't') => string.push('\t'),
455                Some(b'n') => string.push('\n'),
456                Some(b'r') => string.push('\r'),
457                Some(b'"') => string.push('\"'),
458                Some(b'\\') => string.push('\\'),
459                Some(b'x') => {
460                    let hex1 = hex_digit_byte_to_u8(iter.next()?)?;
461                    let hex2 = hex_digit_byte_to_u8(iter.next()?)?;
462                    let chr = (hex1 << 4) | hex2;
463                    if chr > 0x7F {
464                        return None;
465                    }
466                    string.push(char::from(chr));
467                }
468                Some(_) | None => return None,
469            },
470            Some(byte @ b'\x00'..=b'\x7F') => string.push(char::from(byte)),
471            Some(_) => return None,
472        }
473    }
474
475    Some(string)
476}
477
478pub fn decode_utf8_string(atom: &str) -> Option<String> {
479    let mut iter = atom.chars();
480    if iter.next() != Some('"') {
481        return None;
482    }
483
484    let mut string = String::new();
485    loop {
486        match iter.next() {
487            None => return None,
488            Some('"') => {
489                if iter.next().is_some() {
490                    return None;
491                }
492                break;
493            }
494            Some('\\') => match iter.next() {
495                Some('0') => string.push('\0'),
496                Some('t') => string.push('\t'),
497                Some('n') => string.push('\n'),
498                Some('r') => string.push('\r'),
499                Some('"') => string.push('\"'),
500                Some('\\') => string.push('\\'),
501                Some('x') => {
502                    let hex1 = hex_digit_char_to_u8(iter.next()?)?;
503                    let hex2 = hex_digit_char_to_u8(iter.next()?)?;
504                    let chr = (hex1 << 4) | hex2;
505                    if chr > 0x7F {
506                        return None;
507                    }
508                    string.push(char::from(chr));
509                }
510                Some('u') => {
511                    if iter.next() != Some('{') {
512                        return None;
513                    }
514                    let mut num_digits = 0;
515                    let mut esc_chr: u32 = 0;
516                    loop {
517                        match iter.next() {
518                            Some('}') => {
519                                if num_digits == 0 {
520                                    return None;
521                                } else {
522                                    break;
523                                }
524                            }
525                            Some(chr) => {
526                                if num_digits == 6 {
527                                    return None;
528                                }
529                                esc_chr <<= 4;
530                                esc_chr |= u32::from(hex_digit_char_to_u8(chr)?);
531                                num_digits += 1;
532                            }
533                            None => return None,
534                        }
535                    }
536                    string.push(char::try_from(esc_chr).ok()?);
537                }
538                Some(_) | None => return None,
539            },
540            Some(chr) => string.push(chr),
541        }
542    }
543
544    Some(string)
545}