use thiserror::Error;
use yash_env::system::Errno;
use yash_env::Env;
use yash_semantics::expansion::attr::AttrChar;
use yash_semantics::expansion::attr::Origin;
use yash_syntax::source::pretty::AnnotationType;
use yash_syntax::source::pretty::Message;
use yash_syntax::syntax::Fd;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("error reading from the standard input: {errno}")]
pub struct Error {
#[from]
pub errno: Errno,
}
impl Error {
#[must_use]
pub fn to_message(&self) -> Message {
Message {
r#type: AnnotationType::Error,
title: self.to_string().into(),
annotations: vec![],
footers: vec![],
}
}
}
impl<'a> From<&'a Error> for Message<'a> {
#[inline]
fn from(error: &'a Error) -> Self {
error.to_message()
}
}
fn quoted(value: char) -> AttrChar {
AttrChar {
value,
origin: Origin::SoftExpansion,
is_quoted: true,
is_quoting: false,
}
}
fn quoting(value: char) -> AttrChar {
AttrChar {
value,
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: true,
}
}
fn plain(value: char) -> AttrChar {
AttrChar {
value,
origin: Origin::SoftExpansion,
is_quoted: false,
is_quoting: false,
}
}
pub async fn read(env: &mut Env, is_raw: bool) -> Result<(Vec<AttrChar>, bool), Error> {
let mut result = Vec::new();
let newline_found = loop {
match read_char(env).await? {
None => break false,
Some('\n') => break true,
Some('\\') if !is_raw => {
let c = read_char(env).await?;
if c == Some('\n') {
continue;
}
result.push(quoting('\\'));
match c {
None => break false,
Some(c) => result.push(quoted(c)),
}
}
Some(c) => result.push(plain(c)),
}
};
Ok((result, newline_found))
}
async fn read_char(env: &mut Env) -> Result<Option<char>, Error> {
let mut buffer = [0; 4];
let mut len = 0;
loop {
let byte = std::slice::from_mut(&mut buffer[len]);
let count = env.system.read_async(Fd::STDIN, byte).await?;
if count == 0 {
return if len == 0 {
Ok(None)
} else {
Err(Errno::EILSEQ.into())
};
}
debug_assert_eq!(count, 1);
len += 1;
match std::str::from_utf8(&buffer[..len]) {
Ok(s) => {
let mut chars = s.chars();
let c = chars.next().unwrap();
debug_assert_eq!(chars.next(), None);
return Ok(Some(c));
}
Err(e) => match e.error_len() {
None => {
continue;
}
Some(_) => return Err(Errno::EILSEQ.into()),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::in_virtual_system;
use std::cell::RefCell;
use yash_env::system::r#virtual::FileBody;
use yash_env::system::r#virtual::SystemState;
fn set_stdin<B: Into<Vec<u8>>>(system: &RefCell<SystemState>, bytes: B) {
let state = system.borrow_mut();
let stdin = state.file_system.get("/dev/stdin").unwrap();
stdin.borrow_mut().body = FileBody::new(bytes);
}
fn attr_chars(s: &str) -> Vec<AttrChar> {
s.chars().map(plain).collect()
}
#[test]
fn empty_input() {
in_virtual_system(|mut env, _| async move {
let result = read(&mut env, false).await;
assert_eq!(result, Ok((vec![], false)));
})
}
#[test]
fn non_empty_input() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, "foo\nbar\n");
let result = read(&mut env, false).await;
assert_eq!(result, Ok((attr_chars("foo"), true)));
let result = read(&mut env, false).await;
assert_eq!(result, Ok((attr_chars("bar"), true)));
let result = read(&mut env, false).await;
assert_eq!(result, Ok((vec![], false)));
})
}
#[test]
fn input_without_newline() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, "newline");
let result = read(&mut env, false).await;
assert_eq!(result, Ok((attr_chars("newline"), false)));
let result = read(&mut env, false).await;
assert_eq!(result, Ok((vec![], false)));
})
}
#[test]
fn multibyte_characters() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, "©⁉😀\n");
let result = read(&mut env, false).await;
assert_eq!(result, Ok((attr_chars("©⁉😀"), true)));
})
}
#[test]
fn raw_mode() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, "\\foo\\\nbar\\\nbaz\n");
let result = read(&mut env, true).await;
assert_eq!(result, Ok((attr_chars("\\foo\\"), true)));
})
}
#[test]
fn no_raw_mode() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, "\\foo\\\nbar\\\nbaz\n");
let result = read(&mut env, false).await;
assert_eq!(
result,
Ok((
vec![
quoting('\\'),
quoted('f'),
plain('o'),
plain('o'),
plain('b'),
plain('a'),
plain('r'),
plain('b'),
plain('a'),
plain('z'),
],
true,
)),
);
})
}
#[test]
fn orphan_backslash() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, "foo\\");
let result = read(&mut env, false).await;
assert_eq!(
result,
Ok((
vec![plain('f'), plain('o'), plain('o'), quoting('\\')],
false,
)),
);
})
}
#[test]
fn broken_utf8() {
in_virtual_system(|mut env, system| async move {
set_stdin(&system, *b"\xFF");
let result = read(&mut env, false).await;
assert_eq!(result, Err(Errno::EILSEQ.into()));
});
in_virtual_system(|mut env, system| async move {
set_stdin(&system, *b"\xCF\xD0");
let result = read(&mut env, false).await;
assert_eq!(result, Err(Errno::EILSEQ.into()));
});
in_virtual_system(|mut env, system| async move {
set_stdin(&system, *b"\xCF");
let result = read(&mut env, false).await;
assert_eq!(result, Err(Errno::EILSEQ.into()));
});
}
}