#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
#![forbid(unsafe_code)]
use core::fmt;
use core::num::ParseIntError;
#[cfg(all(feature = "alloc", not(feature = "std")))]
use alloc::borrow::Cow;
#[cfg(feature = "std")]
use std::borrow::Cow;
#[derive(Debug, PartialEq, Clone)]
#[non_exhaustive]
pub enum Error {
IncompleteSequence,
IncompleteUnicode,
InvalidUnicode(u32),
UnknownSequence(char),
ParseIntError(ParseIntError),
}
impl From<ParseIntError> for Error {
#[inline]
fn from(this: ParseIntError) -> Self {
Error::ParseIntError(this)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::IncompleteSequence => f.write_str("unexpected end of string after `\\`"),
Self::IncompleteUnicode => {
f.write_str("unexpected end of string in Unicode escape sequence")
}
Self::InvalidUnicode(code) => write!(f, "invalid Unicode character code {code}"),
Self::UnknownSequence(ch) => write!(f, "unknown escape sequence starting with `{ch}`"),
Self::ParseIntError(err) => write!(f, "error parsing integer: {err}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
pub enum StringFragment<'a> {
Raw(&'a str),
Escaped(char),
Empty,
}
impl From<char> for StringFragment<'_> {
#[inline]
fn from(this: char) -> Self {
Self::Escaped(this)
}
}
impl From<Option<char>> for StringFragment<'_> {
#[inline]
fn from(this: Option<char>) -> Self {
match this {
Some(ch) => Self::Escaped(ch),
None => Self::Empty,
}
}
}
impl<'a> From<&'a str> for StringFragment<'a> {
#[inline]
fn from(this: &'a str) -> Self {
Self::Raw(this)
}
}
#[inline]
fn unicode_char(s: &str, chars: usize) -> Result<(char, &str), Error> {
if s.len() < chars || !s.as_bytes()[..chars].is_ascii() {
Err(Error::IncompleteUnicode)
} else {
let num = u32::from_str_radix(&s[0..chars], 16)?;
let ch = char::from_u32(num).ok_or(Error::InvalidUnicode(num))?;
Ok((ch, &s[chars..]))
}
}
pub fn default_escape_sequence(s: &str) -> Result<(char, &str), Error> {
let mut chars = s.chars();
let next = chars.next().ok_or(Error::IncompleteSequence)?;
match next {
'a' => Ok(('\x07', chars.as_str())),
'b' => Ok(('\x08', chars.as_str())),
'f' => Ok(('\x0C', chars.as_str())),
'n' => Ok(('\n', chars.as_str())),
'r' => Ok(('\r', chars.as_str())),
't' => Ok(('\t', chars.as_str())),
'v' => Ok(('\x0B', chars.as_str())),
'\\' | '\'' | '\"' | '/' => Ok((next, chars.as_str())),
'\r' | '\n' => Ok((next, chars.as_str())),
'x' => unicode_char(chars.as_str(), 2),
'u' => {
let s = chars.as_str();
if chars.next() == Some('{') {
let (hex, rest) = chars
.as_str()
.split_once('}')
.ok_or(Error::IncompleteUnicode)?;
let num = u32::from_str_radix(hex, 16)?;
let ch = char::from_u32(num).ok_or(Error::InvalidUnicode(num))?;
Ok((ch, rest))
} else {
unicode_char(s, 4)
}
}
'U' => unicode_char(chars.as_str(), 8),
_ => {
let count = s.chars().take_while(|n| n.is_digit(8)).count().min(3);
if count > 0 {
let num = u32::from_str_radix(&s[0..count], 8)?;
let ch = char::from_u32(num).ok_or(Error::InvalidUnicode(num))?;
Ok((ch, &s[count..]))
} else {
Err(Error::UnknownSequence(next))
}
}
}
}
#[inline]
fn non_empty(s: &str) -> Option<&str> {
if s.is_empty() {
None
} else {
Some(s)
}
}
#[inline]
fn split_at_escape(s: &str) -> (Option<&str>, Option<&str>) {
if let Some((first, last)) = s.split_once('\\') {
(
non_empty(first),
Some(last),
)
} else {
(non_empty(s), None)
}
}
#[derive(Clone)]
pub struct Unescape<'a, F, E, C = Option<char>>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
{
bare: Option<&'a str>,
escaped: Option<&'a str>,
rem: Option<&'a str>,
escape_sequence: F,
}
impl<'a, F, E, C> Unescape<'a, F, E, C>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
{
#[inline]
pub fn new(escape_sequence: F, from: &'a str) -> Self {
let rem = non_empty(from);
let (bare, escaped) = split_at_escape(from);
Self {
bare,
escaped,
rem,
escape_sequence,
}
}
#[inline]
pub fn remainder(&self) -> Option<&'a str> {
self.rem
}
}
impl<'a, F, E, C> fmt::Debug for Unescape<'a, F, E, C>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char> + fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Unescape")
.field("bare", &self.bare)
.field("escaped", &self.escaped)
.field("rem", &self.rem)
.finish_non_exhaustive()
}
}
impl<'a, F, E, C> Unescape<'a, F, E, C>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
StringFragment<'a>: From<C>,
{
#[inline]
pub fn next_fragment(&mut self) -> Option<Result<StringFragment<'a>, E>> {
if let Some(frag) = self.bare.take() {
self.rem = self.rem.and_then(|rem| non_empty(&rem[frag.len()..]));
Some(Ok(StringFragment::Raw(frag)))
} else {
self.next().map(|opt| opt.map(StringFragment::from))
}
}
#[cfg(any(feature = "std", feature = "alloc"))]
pub fn as_cow(&mut self) -> Result<Cow<'a, str>, E> {
let mut out = Cow::default();
while let Some(fragment) = self.next_fragment().transpose()? {
match fragment {
StringFragment::Raw(s) => out += s,
StringFragment::Escaped(c) => out.to_mut().push(c),
StringFragment::Empty => (),
}
}
Ok(out)
}
}
#[cfg(any(feature = "std", feature = "alloc"))]
impl<'a, F, E, C> core::convert::TryFrom<Unescape<'a, F, E, C>> for Cow<'a, str>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
StringFragment<'a>: From<C>,
{
type Error = E;
fn try_from(mut value: Unescape<'a, F, E, C>) -> Result<Self, E> {
value.as_cow()
}
}
impl<'a, F, E, C> Iterator for Unescape<'a, F, E, C>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
{
type Item = Result<C, E>;
fn next(&mut self) -> Option<Self::Item> {
self.bare
.as_mut()
.and_then(|bare| {
let mut chars = bare.chars();
let ch = chars.next()?;
*bare = chars.as_str();
self.rem = self.rem.and_then(|rem| non_empty(&rem[ch.len_utf8()..]));
Some(Ok(C::from(ch)))
})
.or_else(|| {
if let Some(s) = self.escaped.take() {
Some(match (self.escape_sequence)(s) {
Ok((ch, rem)) => {
self.rem = non_empty(rem);
let (bare, escaped) = split_at_escape(rem);
self.bare = bare;
self.escaped = escaped;
Ok(ch)
}
Err(e) => {
self.bare = None;
self.escaped = None;
Err(e)
}
})
} else {
self.bare = None;
None
}
})
}
}
impl<'a, F, E, C> core::iter::FusedIterator for Unescape<'a, F, E, C>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
{
}
pub type UnescapeDefault<'a> =
Unescape<'a, fn(&'a str) -> Result<(char, &'a str), Error>, Error, char>;
#[cfg(any(feature = "std", feature = "alloc"))]
#[inline]
pub fn unescape<'a, F, E, C>(escape_sequence: F, s: &'a str) -> Result<Cow<'a, str>, E>
where
F: FnMut(&'a str) -> Result<(C, &'a str), E>,
C: From<char>,
StringFragment<'a>: From<C>,
{
Unescape::new(escape_sequence, s).as_cow()
}
#[cfg(any(feature = "std", feature = "alloc"))]
#[inline]
pub fn unescape_default(s: &str) -> Result<Cow<'_, str>, Error> {
UnescapeDefault::new(default_escape_sequence, s).as_cow()
}
pub fn split_at_unescaped<'a, F, C>(
escape_sequence: F,
s: &'a str,
split_at: char,
) -> Option<(&'a str, Option<&'a str>)>
where
F: FnMut(&'a str) -> Result<(C, &'a str), Error> + Clone,
C: From<char>,
StringFragment<'a>: From<C>,
{
let start = s.as_ptr().addr();
let mut rem = Some(s);
while let Some(sub) = rem {
let (fst, snd) = sub.split_once(split_at)?;
rem = non_empty(snd);
let range = fst
.as_ptr()
.addr()
.wrapping_add(fst.len())
.wrapping_sub(start);
let slice = &s[..range];
let mut un = Unescape::new(escape_sequence.clone(), fst)
.skip_while(|x| !matches!(x, Err(Error::IncompleteSequence)));
if un.next().is_none() {
return Some((slice, rem));
}
}
None
}
#[cfg(all(test, feature = "std"))]
mod test {
use super::*;
use quickcheck::TestResult;
use quickcheck_macros::quickcheck;
use std::borrow::Cow;
#[test]
fn borrow_strings_without_escapes() {
assert!(matches!(
unescape_default("hello").unwrap(),
Cow::Borrowed(_)
));
assert!(matches!(
unescape_default("longer\nstring").unwrap(),
Cow::Borrowed(_)
));
}
#[test]
fn unescapes_backslashes() {
assert_eq!(unescape_default(r"\\").unwrap(), "\\");
assert_eq!(unescape_default(r"\\\\").unwrap(), "\\\\");
assert_eq!(unescape_default(r"\\\\\\").unwrap(), "\\\\\\");
assert_eq!(unescape_default(r"\\a").unwrap(), "\\a");
assert!(matches!(
unescape_default(r"\\\"),
Err(Error::IncompleteSequence)
));
}
#[test]
fn unicode_escapes() {
assert_eq!(unescape_default(r"\u1234").unwrap(), "\u{1234}");
assert_eq!(unescape_default(r"\u{1234}").unwrap(), "\u{1234}");
assert_eq!(unescape_default(r"\U0010FFFF").unwrap(), "\u{10FFFF}");
assert_eq!(unescape_default(r"\x20").unwrap(), " ");
}
#[test]
fn unicode_multibyte() {
assert!(unescape_default(r"\Uparrow⇑").is_err());
}
#[test]
fn unicode_brace_escapes() {
assert!(unescape_default("\\u{H…H}").is_err()); assert!(unescape_default("\\u{café}").is_err()); assert!(unescape_default("\\u{🦀}").is_err());
assert_eq!(unescape_default(r"\u{41"), Err(Error::IncompleteUnicode));
assert!(matches!(
unescape_default(r"\u{}"),
Err(Error::ParseIntError(_))
));
assert_eq!(unescape_default(r"\u{0}").unwrap(), "\0");
assert_eq!(unescape_default(r"\u{10FFFF}").unwrap(), "\u{10FFFF}");
assert_eq!(
unescape_default(r"\u{D800}"),
Err(Error::InvalidUnicode(0xD800))
);
assert_eq!(
unescape_default(r"\u{110000}"),
Err(Error::InvalidUnicode(0x110000))
);
assert_eq!(unescape_default(r"\u{48}\u{49}").unwrap(), "HI");
}
#[test]
fn split_unescaped() {
assert_eq!(
split_at_unescaped(default_escape_sequence, "abc", '\"'),
None
);
assert_eq!(
split_at_unescaped(default_escape_sequence, "abc'xyz", '\''),
Some(("abc", Some("xyz")))
);
assert_eq!(
split_at_unescaped(default_escape_sequence, "abc'def'ghi", '\''),
Some(("abc", Some("def'ghi")))
);
let split = split_at_unescaped(default_escape_sequence, r#"before\"split"after"#, '\"');
assert_eq!(split, Some((r#"before\"split"#, Some("after"))));
let unesc = unescape_default(split.unwrap().0).unwrap();
assert_eq!(unesc, "before\"split");
assert_eq!(
split_at_unescaped(default_escape_sequence, r#"\\\\\""#, '\"'),
None
);
}
#[quickcheck]
#[cfg_attr(miri, ignore = "slow to run under Miri")]
fn inverts_escape_default(s: String) -> TestResult {
let escaped: String = s.escape_default().collect();
if escaped == s {
return TestResult::discard();
}
let unescaped = unescape_default(&escaped);
match unescaped {
Ok(unescaped) => TestResult::from_bool(s == unescaped),
Err(e) => TestResult::error(e.to_string()),
}
}
}
#[cfg(all(test, not(feature = "std")))]
compile_error!("Tests currently require `std` feature");