use super::error::position_at;
use super::{Error, ErrorKind};
fn error(src: &[u8], offset: usize, kind: ErrorKind) -> Error {
Error::new(kind, position_at(src, offset))
}
fn push_code_point(out: &mut Vec<u8>, cp: u32) {
if cp < 0x80 {
out.push(cp as u8);
} else if cp < 0x800 {
out.push(0xC0 | (cp >> 6) as u8);
out.push(0x80 | (cp & 0x3F) as u8);
} else {
out.push(0xE0 | (cp >> 12) as u8);
out.push(0x80 | ((cp >> 6) & 0x3F) as u8);
out.push(0x80 | (cp & 0x3F) as u8);
}
}
fn hex_value(b: u8) -> Option<u32> {
(b as char).to_digit(16)
}
fn simple_escape(b: u8) -> u8 {
match b {
b'b' => 0x08,
b'f' => 0x0C,
b'n' => b'\n',
b'r' => b'\r',
b't' => b'\t',
other => other,
}
}
pub(crate) fn double_quoted(src: &[u8], start: usize) -> Result<(Vec<u8>, usize), Error> {
let mut out = Vec::new();
let mut i = start + 1;
loop {
let Some(&b) = src.get(i) else {
return Err(error(src, start, ErrorKind::UnterminatedString));
};
match b {
b'"' => return Ok((out, i + 1)),
b'\\' => {
let Some(&next) = src.get(i + 1) else {
return Err(error(src, start, ErrorKind::UnterminatedString));
};
if next <= 0x1E {
return Err(error(
src,
i + 1,
ErrorKind::ControlCharacter { byte: next },
));
}
if next == b'u' {
let digits = src.get(i + 2..i + 6).unwrap_or(&[]);
let cp = (digits.len() == 4)
.then(|| {
digits
.iter()
.try_fold(0u32, |acc, &d| hex_value(d).map(|v| acc * 16 + v))
})
.flatten()
.ok_or_else(|| error(src, i, ErrorKind::InvalidUnicodeEscape))?;
push_code_point(&mut out, cp);
i += 6;
} else {
out.push(simple_escape(next));
i += 2;
}
}
0x00..=0x1E => return Err(error(src, i, ErrorKind::ControlCharacter { byte: b })),
_ => {
out.push(b);
i += 1;
}
}
}
}
pub(crate) fn single_quoted(src: &[u8], start: usize) -> Result<(Vec<u8>, usize), Error> {
let mut out = Vec::new();
let mut i = start + 1;
loop {
let Some(&b) = src.get(i) else {
return Err(error(src, start, ErrorKind::UnterminatedString));
};
match b {
b'\'' => return Ok((out, i + 1)),
b'\\' => match src.get(i + 1) {
None => return Err(error(src, start, ErrorKind::UnterminatedString)),
Some(b'\'') => {
out.push(b'\'');
i += 2;
}
Some(b'\n') => i += 2,
Some(b'\r') if src.get(i + 2) == Some(&b'\n') => i += 3,
Some(b'\r') => i += 2,
Some(&other) => {
out.push(b'\\');
out.push(other);
i += 2;
}
},
_ => {
out.push(b);
i += 1;
}
}
}
}
pub(crate) fn heredoc_opener(src: &[u8], start: usize) -> Option<(usize, usize)> {
let rest = src.get(start..)?;
if rest.len() < 4 || !rest.starts_with(b"<<") {
return None;
}
let name_start = start + 2;
let name_end = name_start
+ src[name_start..]
.iter()
.take_while(|b| b.is_ascii_uppercase())
.count();
(src.get(name_end) == Some(&b'\n')).then_some((name_start, name_end))
}
pub(crate) fn heredoc_opener_cut_by_end(src: &[u8], start: usize) -> bool {
src.get(start..).is_some_and(|rest| {
rest.len() >= 4 && rest.starts_with(b"<<") && rest[2..].iter().all(u8::is_ascii_uppercase)
})
}
#[derive(Debug)]
pub(crate) struct Heredoc {
pub(crate) content: Vec<u8>,
pub(crate) end: usize,
pub(crate) expand: bool,
}
fn ends_terminator(src: &[u8], at: usize) -> bool {
matches!(src.get(at), None | Some(b'\n' | b';' | b','))
}
pub(crate) fn heredoc(src: &[u8], start: usize) -> Result<Heredoc, Error> {
let (name_start, name_end) =
heredoc_opener(src, start).expect("caller checked the heredoc opener");
let name = &src[name_start..name_end];
let content_start = name_end + 1;
if name.is_empty() {
return empty_name_heredoc(src, start, content_start);
}
let repeated = name.iter().all(|&b| b == name[0]);
let mut line = match src[content_start..].iter().position(|&b| b == b'\n') {
Some(n) => content_start + n + 1,
None => return Err(error(src, start, ErrorKind::UnterminatedHeredoc)),
};
loop {
let after_name = line + name.len();
if src[line..].starts_with(name) && ends_terminator(src, after_name) {
return Ok(Heredoc {
content: src[content_start..line - 1].to_vec(),
end: after_name,
expand: true,
});
}
if repeated {
let letters = src[line..].iter().take_while(|&&b| b == name[0]).count();
if letters > name.len() && ends_terminator(src, line + letters) {
let kept = letters - name.len() - 1;
return Ok(Heredoc {
content: src[content_start..line + kept].to_vec(),
end: line + letters,
expand: true,
});
}
}
match src[line..].iter().position(|&b| b == b'\n') {
Some(n) => line += n + 1,
None => return Err(error(src, start, ErrorKind::UnterminatedHeredoc)),
}
}
}
fn empty_name_heredoc(src: &[u8], start: usize, content_start: usize) -> Result<Heredoc, Error> {
let unterminated = || error(src, start, ErrorKind::UnterminatedHeredoc);
let first_end = src[content_start..]
.iter()
.position(|&b| b == b'\n')
.map(|n| content_start + n)
.ok_or_else(unterminated)?;
let end = src[first_end + 1..]
.iter()
.position(|&b| matches!(b, b'\n' | b';' | b','))
.map(|n| first_end + 1 + n)
.ok_or_else(unterminated)?;
Ok(Heredoc {
content: src[content_start..end - 1].to_vec(),
end,
expand: src[content_start..first_end].contains(&b'$'),
})
}
pub(crate) fn decode_unquoted(raw: &[u8]) -> (Vec<u8>, bool) {
let mut out = Vec::with_capacity(raw.len());
let mut i = 0;
while i < raw.len() {
let b = raw[i];
if b != b'\\' {
out.push(b);
i += 1;
continue;
}
let Some(&next) = raw.get(i + 1) else {
out.push(b'\\');
break;
};
if next == b'u' {
let available = raw.len() - (i + 2);
if available >= 3 {
let digits = &raw[i + 2..i + 2 + available.min(4)];
let hex_prefix = digits.iter().take_while(|d| d.is_ascii_hexdigit()).count();
let value = digits[..hex_prefix]
.iter()
.fold(0u32, |acc, &d| acc * 16 + hex_value(d).unwrap_or(0));
let cp = if hex_prefix == 4 { value } else { value * 16 };
push_code_point(&mut out, cp);
i += 2 + digits.len();
} else {
out.push(b'u');
i += 2 + available.min(1);
}
} else {
out.push(simple_escape(next));
i += 2;
}
}
(out, has_unescaped_dollar(raw))
}
fn has_unescaped_dollar(raw: &[u8]) -> bool {
let mut i = 0;
while i < raw.len() {
match raw[i] {
b'\\' => i += 2,
b'$' => return true,
_ => i += 1,
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn dq(s: &str) -> Result<String, ErrorKind> {
double_quoted(s.as_bytes(), 0)
.map(|(b, _)| String::from_utf8_lossy(&b).into_owned())
.map_err(|e| e.kind().clone())
}
#[test]
fn double_quoted_escapes() {
assert_eq!(dq(r#""a\"b\\c\/d""#).unwrap(), "a\"b\\c/d");
assert_eq!(dq(r#""\b\f\n\r\t""#).unwrap(), "\u{8}\u{c}\n\r\t");
assert_eq!(dq(r#""\u0041\u00e9\u20AC""#).unwrap(), "Aé€");
assert_eq!(dq(r#""\u1F640""#).unwrap(), "\u{1F64}0");
assert_eq!(dq(r#""\q\$x""#).unwrap(), "q$x");
assert_eq!(dq("\"\x1f\x7f\"").unwrap(), "\x1f\x7f");
}
#[test]
fn double_quoted_errors() {
assert_eq!(dq(r#""\uZZZZ""#), Err(ErrorKind::InvalidUnicodeEscape));
assert_eq!(dq(r#""\u12""#), Err(ErrorKind::InvalidUnicodeEscape));
assert_eq!(
dq("\"a\tb\""),
Err(ErrorKind::ControlCharacter { byte: b'\t' })
);
assert_eq!(
dq("\"a\\\nb\""),
Err(ErrorKind::ControlCharacter { byte: b'\n' })
);
assert_eq!(dq("\"abc"), Err(ErrorKind::UnterminatedString));
assert_eq!(dq("\"abc\\"), Err(ErrorKind::UnterminatedString));
}
#[test]
fn surrogate_escape_is_not_utf8() {
let (bytes, _) = double_quoted(br#""\uD83D""#, 0).unwrap();
assert!(std::str::from_utf8(&bytes).is_err());
}
#[test]
fn single_quoted_rules() {
let sq = |s: &str| {
single_quoted(s.as_bytes(), 0)
.map(|(b, _)| String::from_utf8(b).unwrap())
.map_err(|e| e.kind().clone())
};
assert_eq!(sq(r"'it\'s'").unwrap(), "it's");
assert_eq!(sq("'one\\\ntwo'").unwrap(), "onetwo");
assert_eq!(sq("'one\\\r\ntwo'").unwrap(), "onetwo");
assert_eq!(sq("'x\\\ry'").unwrap(), "xy");
assert_eq!(sq("'x\\\r\ry'").unwrap(), "x\ry");
assert_eq!(sq("'x\\\r\r\ny'").unwrap(), "x\r\ny");
assert_eq!(sq("'x\\\r'").unwrap(), "x");
assert_eq!(sq("'x\\\\\ry'").unwrap(), "x\\\\\ry");
assert_eq!(sq("'x\\\\\\\ry'").unwrap(), "x\\\\y");
assert_eq!(sq(r"'x\ny\\z'").unwrap(), r"x\ny\\z");
assert_eq!(sq("'x\ny'").unwrap(), "x\ny");
assert_eq!(sq("'x"), Err(ErrorKind::UnterminatedString));
assert_eq!(sq("'x\\"), Err(ErrorKind::UnterminatedString));
}
fn hd(s: &str) -> Result<(String, usize), ErrorKind> {
heredoc(s.as_bytes(), 0)
.map(|h| (String::from_utf8(h.content).unwrap(), h.end))
.map_err(|e| e.kind().clone())
}
#[test]
fn heredoc_repeated_letter_name() {
assert_eq!(hd("<<A\nx\nAA\n").unwrap(), ("x\n".into(), 8));
assert_eq!(hd("<<A\nx\nAAA\n").unwrap(), ("x\nA".into(), 9));
assert_eq!(hd("<<EE\nx\nEEE\n").unwrap().0, "x\n");
assert_eq!(hd("<<AA\nx\nAAAAA\n").unwrap().0, "x\nAA");
assert_eq!(hd("<<A\nx\nAA;").unwrap(), ("x\n".into(), 8));
assert_eq!(hd("<<A\nx\nAA").unwrap().0, "x\n");
assert_eq!(hd("<<A\nx\nAAB\nA\n").unwrap().0, "x\nAAB");
assert_eq!(hd("<<A\nAA\nx\nA\n").unwrap().0, "AA\nx");
assert_eq!(hd("<<AAA\nx\nA\nAAA\n").unwrap().0, "x\nA");
assert_eq!(hd("<<AB\nx\nABB\nAB\n").unwrap().0, "x\nABB");
assert_eq!(hd("<<A\nx\nAA}\n"), Err(ErrorKind::UnterminatedHeredoc));
}
#[test]
fn heredoc_empty_name() {
assert_eq!(hd("<<\nx\n\n").unwrap(), ("x".into(), 5));
assert_eq!(hd("<<\n\n\n").unwrap(), ("".into(), 4));
assert_eq!(hd("<<\nx\n;").unwrap(), ("x".into(), 5));
assert_eq!(hd("<<\na\nb\n").unwrap(), ("a\n".into(), 6));
assert_eq!(hd("<<\nab\ncd\n").unwrap().0, "ab\nc");
assert_eq!(hd("<<\nx\ny;\n").unwrap(), ("x\n".into(), 6));
assert_eq!(hd("<<\nx\ny,1").unwrap(), ("x\n".into(), 6));
assert_eq!(hd("<<\n;x\n\n").unwrap().0, ";x");
assert_eq!(hd("<<\nx;\n\n").unwrap().0, "x;");
assert_eq!(hd("<<\nx\ny}\n").unwrap().0, "x\ny");
for unterminated in ["<<\ncontent\n", "<<\nx\ny", "<<\n\n", "<<\nx"] {
assert_eq!(
hd(unterminated),
Err(ErrorKind::UnterminatedHeredoc),
"{unterminated:?}"
);
}
let expand = |s: &str| heredoc(s.as_bytes(), 0).unwrap().expand;
assert!(!expand("<<\na\n${ABI}x\n"));
assert!(expand("<<\n$ABI\n${ABI}x\n"));
assert!(expand("<<EOD\na\nEOD\n"));
}
#[test]
fn heredoc_rules() {
assert_eq!(hd("<<EOD\na\nb\nEOD\n").unwrap(), ("a\nb".into(), 13));
assert_eq!(hd("<<EOD\nEOD\nx\nEOD\n").unwrap().0, "EOD\nx");
assert_eq!(hd("<<EOD\n\nEOD\n").unwrap().0, "");
assert_eq!(hd("<<EOD\nx\nEOD").unwrap().0, "x");
assert_eq!(hd("<<EOD\nx\nEOD;").unwrap(), ("x".into(), 11));
assert_eq!(hd("<<\nx\n\n").unwrap().0, "x");
assert_eq!(hd("<<EOD\nx\r\nEOD\n").unwrap().0, "x\r");
assert_eq!(hd("<<EOD\n EOD\nEODX\nEOD\n").unwrap().0, " EOD\nEODX");
assert_eq!(hd("<<EOD\nEOD\n"), Err(ErrorKind::UnterminatedHeredoc));
assert_eq!(hd("<<EOD\nx\nEOD \n"), Err(ErrorKind::UnterminatedHeredoc));
assert_eq!(hd("<<EOD\nx\nEOD}\n"), Err(ErrorKind::UnterminatedHeredoc));
}
#[test]
fn heredoc_openers() {
assert!(heredoc_opener(b"<<EOD\n", 0).is_some());
assert!(heredoc_opener(b"<<\nx", 0).is_some());
assert!(heredoc_opener(b"<<\n", 0).is_none());
assert!(heredoc_opener(b"<<eod\n", 0).is_none());
assert!(heredoc_opener(b"<<EOD \n", 0).is_none());
assert!(heredoc_opener(b"<<EOD\r\n", 0).is_none());
}
#[test]
fn heredoc_openers_cut_by_the_end() {
for cut in [&b"<<EO"[..], b"<<AA", b"<<EOD", b"k = <<ABCDEF"] {
let start = cut.windows(2).position(|w| w == b"<<").unwrap();
assert!(heredoc_opener_cut_by_end(cut, start), "{cut:?}");
}
for not in [
&b"<<E"[..],
b"<<",
b"<<EO ",
b"<<EO;",
b"<<AB1",
b"<<Ab",
b"<<ab",
b"<<EOD]",
b"<<EOD\n",
] {
assert!(!heredoc_opener_cut_by_end(not, 0), "{not:?}");
}
}
fn unq(s: &str) -> String {
String::from_utf8(decode_unquoted(s.as_bytes()).0).unwrap()
}
#[test]
fn unquoted_escapes() {
assert_eq!(unq(r"x\;y\#z\,w"), "x;y#z,w");
assert_eq!(unq(r#"x\ty\"z"#), "x\ty\"z");
assert_eq!(unq(r"a\u0041b"), "aAb");
assert_eq!(unq(r"x\q"), "xq");
assert_eq!(unq(r"ends\"), r"ends\");
assert_eq!(unq("x\\\ny"), "x\ny");
}
#[test]
fn unquoted_invalid_unicode_quirk() {
assert_eq!(unq(r"x\uZZZZ"), "x\u{0}");
assert_eq!(unq(r"x\u00ZZ"), "x\u{0}");
assert_eq!(unq(r"x\u4Z00y"), "x@y");
assert_eq!(unq(r"x\u1ZZZ"), "x\u{10}");
assert_eq!(unq(r"x\u12ZZ"), "x\u{120}");
assert_eq!(unq(r"x\u123Z"), "x\u{1230}");
}
#[test]
fn unquoted_short_unicode_escapes() {
assert_eq!(unq(r"x\u123"), "x\u{1230}");
assert_eq!(unq(r"x\u1Z2"), "x\u{10}");
assert_eq!(unq(r"x\u12\"), "x\u{120}");
assert_eq!(unq(r"x\u12"), "xu2");
assert_eq!(unq(r"x\u41"), "xu1");
assert_eq!(unq(r"x\u1"), "xu");
assert_eq!(unq(r"x\uZ"), "xu");
assert_eq!(unq(r"x\u"), "xu");
assert_eq!(unq(r"a\u\n"), "aun");
assert_eq!(unq(r"x\u1\"), "xu\\");
}
#[test]
fn unquoted_dollars_decide_expansion() {
let expands = |s: &str| decode_unquoted(s.as_bytes()).1;
assert_eq!(decode_unquoted(br"\$A$B").0, b"$A$B");
assert!(expands(r"\$A$B"));
assert!(!expands(r"\$A\$B"));
assert!(!expands(r"\\\$ABI"));
assert!(expands(r"\\$ABI"));
assert!(expands(r"\$ABI\u$000"));
assert!(!expands("plain"));
assert!(!expands(r"\$ABI\u\$"));
assert_eq!(decode_unquoted(br"\$ABI\u\$").0, b"$ABIu$");
assert!(!expands(r"\$ABI\u1\$"));
assert!(!expands(r"\u\$ABI"));
assert!(expands(r"\$ABI\u\\$"));
}
}