Skip to main content

unescaper/
lib.rs

1#![warn(missing_docs)]
2
3//! Unescape the given string.
4//! This is the opposite operation of [`std::ascii::escape_default`].
5
6// crates.io
7use thiserror::Error as ThisError;
8
9#[cfg(test)] mod test;
10
11/// Unescaper's `Result`.
12pub type Result<T> = ::std::result::Result<T, Error>;
13
14/// Unescaper's `Error`.
15#[allow(missing_docs)]
16#[cfg_attr(test, derive(PartialEq, Eq))]
17#[derive(Debug, ThisError)]
18pub enum Error {
19	#[error("incomplete str, break at {0}")]
20	IncompleteStr(usize),
21	#[error("invalid char, {char:?} break at {pos}")]
22	InvalidChar { char: char, pos: usize },
23	#[error("parse int error, break at {pos}")]
24	ParseIntError { source: ::std::num::ParseIntError, pos: usize },
25}
26use Error::*;
27
28/// Unescaper struct which holding the chars cache for unescaping.
29#[derive(Debug)]
30pub struct Unescaper {
31	/// [`str`] cache, in reverse order.
32	pub chars: Vec<char>,
33}
34impl Unescaper {
35	/// Build a new [`Unescaper`] from the given [`str`].
36	pub fn new(s: &str) -> Self {
37		Self { chars: s.chars().rev().collect() }
38	}
39
40	/// Unescape the given [`str`].
41	pub fn unescape(&mut self) -> Result<String> {
42		let chars_count = self.chars.len();
43		let offset = |mut e, remaining_count| {
44			let (IncompleteStr(pos) | InvalidChar { pos, .. } | ParseIntError { pos, .. }) = &mut e;
45
46			*pos += chars_count - remaining_count - 1;
47
48			e
49		};
50		let mut unescaped = String::new();
51
52		while let Some(c) = self.chars.pop() {
53			if c != '\\' {
54				unescaped.push(c);
55
56				continue;
57			}
58
59			let c = self.chars.pop().ok_or(IncompleteStr(chars_count - self.chars.len() - 1))?;
60			let c = match c {
61				'b' => '\u{0008}',
62				'f' => '\u{000c}',
63				'n' => '\n',
64				'r' => '\r',
65				't' => '\t',
66				// https://github.com/hack-ink/unescaper/pull/10#issuecomment-1676443635
67				//
68				// https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf
69				// On page 4 it says: "\/ represents the solidus character (U+002F)."
70				'\'' | '\"' | '\\' | '/' => c,
71				'u' => self.unescape_unicode_internal().map_err(|e| offset(e, self.chars.len()))?,
72				'x' => self.unescape_byte_internal().map_err(|e| offset(e, self.chars.len()))?,
73				_ => self.unescape_octal_internal(c).map_err(|e| offset(e, self.chars.len()))?,
74			};
75
76			unescaped.push(c);
77		}
78
79		Ok(unescaped)
80	}
81
82	// pub fn unescape_unicode(&mut self) -> Result<char> {}
83	fn unescape_unicode_internal(&mut self) -> Result<char> {
84		let c = self.chars.pop().ok_or(Error::IncompleteStr(0))?;
85		let mut unicode = String::new();
86
87		// \u + { + regex(d*) + }
88		if c == '{' {
89			while let Some(n) = self.chars.pop() {
90				if n == '}' {
91					break;
92				}
93
94				unicode.push(n);
95			}
96		}
97		// \u + regex(d{4})
98		else {
99			// [0, 65536), 16^4
100			unicode.push(c);
101
102			for i in 0..3 {
103				let c = self.chars.pop().ok_or(IncompleteStr(i))?;
104
105				unicode.push(c);
106			}
107		}
108
109		char::from_u32(
110			u32::from_str_radix(&unicode, 16).map_err(|e| ParseIntError { source: e, pos: 0 })?,
111		)
112		.ok_or(Error::InvalidChar {
113			char: unicode.chars().last().expect("empty unicode will exit earlier; qed"),
114			pos: 0,
115		})
116	}
117
118	// pub fn unescape_byte(&mut self) -> Result<char> {}
119	fn unescape_byte_internal(&mut self) -> Result<char> {
120		let mut byte = String::new();
121
122		// [0, 256), 16^2
123		for i in 0..2 {
124			let c = self.chars.pop().ok_or(IncompleteStr(i))?;
125
126			byte.push(c);
127		}
128
129		Ok(u8::from_str_radix(&byte, 16).map_err(|e| ParseIntError { source: e, pos: 0 })? as _)
130	}
131
132	// pub fn unescape_octal(&mut self) -> Result<char> {}
133	fn unescape_octal_internal(&mut self, c: char) -> Result<char> {
134		let mut octal = String::new();
135		let mut try_push_next = |octal: &mut String| {
136			if let Some(c) =
137				self.chars.last().cloned().filter(|c| c.is_digit(8)).and_then(|_| self.chars.pop())
138			{
139				octal.push(c);
140			}
141		};
142
143		match c {
144			// decimal [0, 256) == octal [0, 400)
145			// 0 <= first digit < 4
146			// \ + regex(d{1,3})
147			'0' | '1' | '2' | '3' => {
148				octal.push(c);
149
150				(0..2).for_each(|_| try_push_next(&mut octal));
151			},
152			// \ + regex(d{1,2})
153			'4' | '5' | '6' | '7' => {
154				octal.push(c);
155
156				try_push_next(&mut octal);
157			},
158			_ => Err(InvalidChar { char: c, pos: 0 })?,
159		}
160
161		Ok(u8::from_str_radix(&octal, 8).map_err(|e| ParseIntError { source: e, pos: 0 })? as _)
162	}
163}
164
165/// Unescape the given [`str`].
166pub fn unescape(s: &str) -> Result<String> {
167	Unescaper::new(s).unescape()
168}