use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
use std::io::{self, Read};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tabnas::Tabnas;
use crate::doc::Doc;
use crate::grammar::{self, CustomId};
use crate::prov;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Format {
Json,
Jsonl,
Jsonic,
Jsonc,
Json5,
Yaml,
Toml,
Ini,
Csv,
Tsv,
Xml,
Zon,
Markdown,
Feed,
Css,
Proto,
Pgn,
Expr,
Semver,
Text,
Custom(CustomId),
}
impl Format {
pub const ALL: [Format; 20] = [
Format::Json,
Format::Jsonl,
Format::Jsonic,
Format::Jsonc,
Format::Json5,
Format::Yaml,
Format::Toml,
Format::Ini,
Format::Csv,
Format::Tsv,
Format::Xml,
Format::Zon,
Format::Markdown,
Format::Feed,
Format::Css,
Format::Proto,
Format::Pgn,
Format::Expr,
Format::Semver,
Format::Text,
];
pub fn name(self) -> &'static str {
match self {
Format::Json => "json",
Format::Jsonl => "jsonl",
Format::Jsonic => "jsonic",
Format::Jsonc => "jsonc",
Format::Json5 => "json5",
Format::Yaml => "yaml",
Format::Toml => "toml",
Format::Ini => "ini",
Format::Csv => "csv",
Format::Tsv => "tsv",
Format::Xml => "xml",
Format::Zon => "zon",
Format::Markdown => "markdown",
Format::Feed => "feed",
Format::Css => "css",
Format::Proto => "proto",
Format::Pgn => "pgn",
Format::Expr => "expr",
Format::Semver => "semver",
Format::Text => "text",
Format::Custom(id) => grammar::get(id).map_or("custom", |g| g.name),
}
}
pub fn is_custom(self) -> bool {
matches!(self, Format::Custom(_))
}
pub fn keyed_by_header(&self) -> bool {
matches!(self, Format::Csv | Format::Tsv)
}
pub fn from_name(s: &str) -> Option<Format> {
let s = s.trim().to_lowercase();
grammar::lookup(&s)
.map(Format::Custom)
.or_else(|| Format::ALL.iter().copied().find(|f| f.name() == s))
.or_else(|| Format::from_extension(&s))
}
pub fn known_names() -> Vec<&'static str> {
let mut names: Vec<&'static str> = Format::ALL.iter().map(|f| f.name()).collect();
for name in grammar::names() {
if !names
.iter()
.any(|n| n.to_lowercase() == name.to_lowercase())
{
names.push(name);
}
}
names
}
pub fn extensions(self) -> &'static [&'static str] {
match self {
Format::Json => &["json", "geojson", "har", "jsonld", "webmanifest"],
Format::Jsonl => &["jsonl", "ndjson"],
Format::Jsonic => &["jsonic"],
Format::Jsonc => &["jsonc"],
Format::Json5 => &["json5"],
Format::Yaml => &["yaml", "yml"],
Format::Toml => &["toml"],
Format::Ini => &["ini", "cfg", "conf", "cnf"],
Format::Csv => &["csv"],
Format::Tsv => &["tsv", "tab"],
Format::Xml => &["xml", "svg", "xhtml", "xsd", "xsl", "xslt", "plist"],
Format::Zon => &["zon"],
Format::Markdown => &["md", "markdown"],
Format::Feed => &["rss", "atom"],
Format::Css => &["css"],
Format::Proto => &["proto"],
Format::Pgn => &["pgn"],
Format::Expr | Format::Semver => &[],
Format::Text => &["txt", "text", "log"],
Format::Custom(_) => &[],
}
}
pub fn from_extension(ext: &str) -> Option<Format> {
if let Some(id) = grammar::lookup(ext) {
return Some(Format::Custom(id));
}
let ext = ext.to_ascii_lowercase();
Format::ALL
.into_iter()
.find(|f| f.extensions().contains(&ext.as_str()))
}
pub fn detect(path: &Path) -> Format {
Format::detect_known(path).unwrap_or(Format::Text)
}
pub fn detect_known(path: &Path) -> Option<Format> {
let whole = path
.file_name()
.and_then(|n| n.to_str())
.and_then(grammar::lookup)
.map(Format::Custom);
whole.or_else(|| {
path.extension()
.and_then(|e| e.to_str())
.and_then(Format::from_extension)
})
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoadError {
pub message: String,
pub code: String,
pub line: u32,
pub col: u32,
pub hint: Box<str>,
pub source_line: Option<Box<str>>,
pub report: String,
}
pub const DEFAULT_MAX_SIZE: u64 = 64 << 20;
pub const DEFAULT_MAX_OUTPUT: u64 = 1 << 30;
pub const MEMORY_PER_BYTE: u64 = 40;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Limits {
pub max_size: Option<u64>,
pub timeout: Option<Duration>,
pub started: Option<Instant>,
}
impl Limits {
pub const DEFAULT: Limits = Limits {
max_size: Some(DEFAULT_MAX_SIZE),
timeout: None,
started: None,
};
pub const NONE: Limits = Limits {
max_size: None,
timeout: None,
started: None,
};
pub fn current() -> Limits {
let set = |n: u64| (n > 0).then_some(n);
Limits {
max_size: set(MAX_SIZE.load(Ordering::Relaxed)),
timeout: set(TIMEOUT_MS.load(Ordering::Relaxed)).map(Duration::from_millis),
started: None,
}
}
}
impl Default for Limits {
fn default() -> Limits {
Limits::DEFAULT
}
}
static MAX_SIZE: AtomicU64 = AtomicU64::new(DEFAULT_MAX_SIZE);
static TIMEOUT_MS: AtomicU64 = AtomicU64::new(0);
pub fn set_limits(limits: Limits) {
MAX_SIZE.store(limits.max_size.unwrap_or(0), Ordering::Relaxed);
TIMEOUT_MS.store(timeout_ms(limits.timeout), Ordering::Relaxed);
}
fn timeout_ms(timeout: Option<Duration>) -> u64 {
timeout.map_or(0, |t| t.as_millis().clamp(1, u128::from(u64::MAX)) as u64)
}
pub fn parse_timeout(text: &str) -> Result<Option<Duration>, String> {
let t = text.trim();
let bad = || format!("--timeout needs seconds such as 30, 2.5 or 0, not {text:?}");
let (number, scale) = match t.strip_suffix('m') {
Some(n) => (n, 60.0),
None => (t.strip_suffix('s').unwrap_or(t), 1.0),
};
let secs: f64 = number.trim().parse().map_err(|_| bad())?;
if !secs.is_finite() || secs < 0.0 {
return Err(bad());
}
if secs == 0.0 {
return Ok(None);
}
Duration::try_from_secs_f64(secs * scale)
.map(Some)
.map_err(|_| bad())
}
fn seconds(t: Duration) -> String {
t.as_secs_f64().to_string()
}
pub(crate) fn input_timed_out_words(limit: Duration) -> (String, String) {
let limit = seconds(limit);
(
format!("timeout: the input was still being read after {limit} s"),
format!(
"The input had not ended when --timeout {limit} passed: whatever writes it is \
slow, or waiting.\nPass a larger --timeout to wait for it, or --timeout 0 for no \
limit."
),
)
}
pub fn parse_size(text: &str) -> Result<Option<u64>, String> {
parse_size_for("--max-size", text)
}
pub fn parse_size_for(flag: &str, text: &str) -> Result<Option<u64>, String> {
let t = text.trim();
let digits = t.find(|c: char| !c.is_ascii_digit()).unwrap_or(t.len());
let bad = || format!("{flag} needs a size such as 64M, 1G or 0, not {text:?}");
let n: u64 = t[..digits].parse().map_err(|_| bad())?;
let shift = match t[digits..].trim().to_ascii_lowercase().as_str() {
"" | "b" => 0,
"k" | "kb" | "kib" => 10,
"m" | "mb" | "mib" => 20,
"g" | "gb" | "gib" => 30,
_ => return Err(bad()),
};
let bytes = n.checked_mul(1 << shift).ok_or_else(bad)?;
Ok((bytes > 0).then_some(bytes))
}
pub fn size_flag(bytes: u64) -> String {
for (shift, unit) in [(30, "G"), (20, "M"), (10, "K")] {
if bytes >= 1 << shift && bytes.is_multiple_of(1 << shift) {
return format!("{}{unit}", bytes >> shift);
}
}
bytes.to_string()
}
pub fn read_within(mut input: impl Read, max: Option<u64>) -> Result<Vec<u8>, LoadError> {
let mut buf = Vec::new();
let read = match max {
Some(max) => input.take(max.saturating_add(1)).read_to_end(&mut buf),
None => input.read_to_end(&mut buf),
};
read.map_err(|e| match read_timed_out(&e) {
Some(limit) => LoadError::input_timed_out(limit),
None => LoadError::new(e.to_string()),
})?;
match max {
Some(max) if buf.len() as u64 > max => Err(LoadError::too_large(None, max)),
_ => Ok(buf),
}
}
pub struct DeadlineReader {
input: Option<Box<dyn Read + Send>>,
chunks: Option<mpsc::Receiver<io::Result<Vec<u8>>>>,
chunk: Vec<u8>,
at: usize,
deadline: Instant,
limit: Duration,
ended: bool,
}
const CHUNKS_AHEAD: usize = 4;
const CHUNK_BYTES: usize = 64 << 10;
impl DeadlineReader {
pub fn new(
input: impl Read + Send + 'static,
started: Instant,
limit: Duration,
) -> Option<DeadlineReader> {
Some(DeadlineReader {
input: Some(Box::new(input)),
chunks: None,
chunk: Vec::new(),
at: 0,
deadline: started.checked_add(limit)?,
limit,
ended: false,
})
}
fn start(&mut self) -> io::Result<()> {
let Some(mut input) = self.input.take() else {
return Ok(());
};
let (send, chunks) = mpsc::sync_channel(CHUNKS_AHEAD);
std::thread::Builder::new()
.name("aless-input".into())
.spawn(move || loop {
let mut buf = vec![0; CHUNK_BYTES];
let read = match input.read(&mut buf) {
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Ok(n) => {
buf.truncate(n);
Ok(buf)
}
Err(e) => Err(e),
};
let last = !matches!(&read, Ok(b) if !b.is_empty());
if send.send(read).is_err() || last {
break;
}
})
.map_err(|e| {
io::Error::new(
e.kind(),
format!("no thread could be started to read the input within --timeout: {e}"),
)
})?;
self.chunks = Some(chunks);
Ok(())
}
}
impl Read for DeadlineReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
self.start()?;
let Some(chunks) = &self.chunks else {
unreachable!("started above");
};
while self.at == self.chunk.len() {
if self.ended {
return Ok(0);
}
let now = Instant::now();
if now >= self.deadline {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
ReadTimedOut(self.limit),
));
}
match chunks.recv_timeout(self.deadline - now) {
Ok(Ok(chunk)) if chunk.is_empty() => self.ended = true,
Ok(Ok(chunk)) => {
self.chunk = chunk;
self.at = 0;
}
Ok(Err(e)) => {
self.ended = true;
return Err(e);
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => self.ended = true,
}
}
let n = buf.len().min(self.chunk.len() - self.at);
buf[..n].copy_from_slice(&self.chunk[self.at..self.at + n]);
self.at += n;
Ok(n)
}
}
#[derive(Debug)]
struct ReadTimedOut(Duration);
impl fmt::Display for ReadTimedOut {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"the input was still being read when --timeout {} passed",
seconds(self.0)
)
}
}
impl std::error::Error for ReadTimedOut {}
#[cfg(test)]
pub(crate) fn quiet(first: &[u8]) -> impl Read + Send + 'static {
struct Quiet {
first: Vec<u8>,
wait: mpsc::Receiver<()>,
_held: mpsc::Sender<()>,
}
impl Read for Quiet {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.first.is_empty() {
let _ = self.wait.recv();
return Ok(0);
}
let n = buf.len().min(self.first.len());
buf[..n].copy_from_slice(&self.first[..n]);
self.first.drain(..n);
Ok(n)
}
}
let (held, wait) = mpsc::channel();
Quiet {
first: first.to_vec(),
wait,
_held: held,
}
}
pub(crate) fn read_timed_out(e: &io::Error) -> Option<Duration> {
e.get_ref()?.downcast_ref::<ReadTimedOut>().map(|t| t.0)
}
pub const MAX_RULE_DEPTH: usize = 3_000;
pub const MAX_VALUE_DEPTH: usize = MAX_RULE_DEPTH / 3;
pub const RULE_HISTORY: usize = 3;
pub(crate) const PARSE_STACK: usize = 64 << 20;
pub(crate) const STOP_DEPTH: u8 = 1;
pub(crate) const STOP_TIME: u8 = 2;
pub(crate) const STOP_VALUE_DEPTH: u8 = 3;
#[derive(Clone)]
pub(crate) struct Deadline {
at: Instant,
limit: Duration,
alarm: Option<Arc<AtomicBool>>,
cancel: Option<Arc<AtomicBool>>,
}
impl Deadline {
pub(crate) fn after(timeout: Option<Duration>) -> Option<Deadline> {
let limit = timeout?;
Some(Deadline {
at: Instant::now().checked_add(limit)?,
limit,
alarm: None,
cancel: None,
})
}
pub(crate) fn or_cancelled(self, cancel: Arc<AtomicBool>) -> Deadline {
Deadline {
cancel: Some(cancel),
..self
}
}
pub(crate) fn passed(&self) -> bool {
if self
.cancel
.as_ref()
.is_some_and(|cancel| cancel.load(Ordering::Relaxed))
{
return true;
}
match &self.alarm {
Some(alarm) => alarm.load(Ordering::Relaxed),
None => Instant::now() >= self.at,
}
}
}
#[derive(Debug)]
pub(crate) struct Stop {
why: AtomicU8,
line: AtomicU64,
col: AtomicU64,
token: Mutex<Option<(usize, String, String)>>,
}
impl Stop {
fn new() -> Arc<Stop> {
Arc::new(Stop {
why: AtomicU8::new(0),
line: AtomicU64::new(0),
col: AtomicU64::new(0),
token: Mutex::new(None),
})
}
fn record(&self, why: u8, ctx: &tabnas::Context) {
if let Some(t) = ctx.t0().or_else(|| ctx.v1()) {
self.line.store(t.site.ri as u64, Ordering::Relaxed);
self.col.store(t.site.ci as u64, Ordering::Relaxed);
*self.token.lock().unwrap_or_else(|e| e.into_inner()) =
Some((t.site.pos, t.name.to_string(), t.src.to_string()));
}
self.why.store(why, Ordering::Relaxed);
}
fn place(&self, e: &mut tabnas::TabnasError) {
let token = self.token.lock().unwrap_or_else(|e| e.into_inner());
if let Some((pos, name, src)) = token.as_ref() {
let (line, col) = self.position();
e.pos = *pos;
e.row = line as usize;
e.col = col as usize;
e.len = src.chars().count();
e.src = src.clone();
e.token.name = name.clone();
e.token.src = src.clone();
}
}
pub(crate) fn mark(&self, why: u8) {
self.why.store(why, Ordering::Relaxed);
}
pub(crate) fn why(&self) -> u8 {
self.why.load(Ordering::Relaxed)
}
pub(crate) fn position(&self) -> (u32, u32) {
let at = |n: &AtomicU64| u32::try_from(n.load(Ordering::Relaxed)).unwrap_or(u32::MAX);
(at(&self.line), at(&self.col))
}
}
const NO_FILE: &str = "<no-file>";
const END_TOKEN: &str = "#ZZ";
const END_HINT: &str = "The document ends before it is complete: look for an unclosed\n\
bracket, brace or string, or a missing value at the end.";
impl LoadError {
pub fn new(message: impl Into<String>) -> LoadError {
LoadError::tagged("io", message)
}
pub fn tagged(tag: &str, message: impl Into<String>) -> LoadError {
let message = message.into();
let report = format!(
"\x1b[91m[aless/{tag}]:\x1b[0m {}\n \x1b[34m-->\x1b[0m {NO_FILE}",
escape_controls(&message)
);
LoadError {
message,
code: tag.to_string(),
line: 0,
col: 0,
hint: Box::default(),
source_line: None,
report,
}
}
pub(crate) fn refused(
format: Format,
code: &str,
message: impl Into<String>,
hint: &str,
) -> LoadError {
let mut e = LoadError::tagged(code, message);
e.report = e
.report
.replacen(&format!("[aless/{code}]"), &format!("[{format}/{code}]"), 1);
e.with_hint(hint)
}
pub fn is_io(&self) -> bool {
self.code == "io"
}
pub fn is_too_large(&self) -> bool {
self.code == "too_large"
}
pub fn is_timeout(&self) -> bool {
self.code == "timeout"
}
pub(crate) fn input_timed_out(limit: Duration) -> LoadError {
let (message, hint) = input_timed_out_words(limit);
LoadError::tagged("timeout", message).with_hint(&hint)
}
pub fn too_large(size: Option<u64>, limit: u64) -> LoadError {
use crate::explorer::human_size;
let message = match size {
Some(n) => format!(
"input is {}, over the {} limit",
human_size(n),
human_size(limit)
),
None => format!("input is over the {} limit", human_size(limit)),
};
let per_byte =
format!("Parsing takes about {MEMORY_PER_BYTE} bytes of memory per byte of input");
let hint = match size {
Some(n) => format!(
"{per_byte}, so this one would need about {}. Pass --max-size {} \
(or more) to read it, or --max-size 0 for no limit.",
human_size(n.saturating_mul(MEMORY_PER_BYTE)),
size_flag(room_for(size, limit))
),
None => format!(
"{per_byte}. Pass a larger --max-size, such as {}, to read it, or \
--max-size 0 for no limit.",
size_flag(room_for(size, limit))
),
};
LoadError::tagged("too_large", message).with_hint(&hint)
}
pub(crate) fn grammar_too_large(size: Option<u64>, limit: u64) -> LoadError {
LoadError::file_too_large("the grammar file", "A grammar file", size, limit)
}
pub(crate) fn program_too_large(size: Option<u64>, limit: u64) -> LoadError {
LoadError::file_too_large("program", "A program file", size, limit)
}
fn file_too_large(what: &str, which: &str, size: Option<u64>, limit: u64) -> LoadError {
use crate::explorer::human_size;
let message = match size {
Some(n) => format!(
"{what} is {}, over the {} limit",
human_size(n),
human_size(limit)
),
None => format!("{what} is over the {} limit", human_size(limit)),
};
let hint = format!(
"{which} over --max-size is not read. Pass --max-size {} (or more) to read it, \
or --max-size 0 for no limit.",
size_flag(room_for(size, limit))
);
LoadError::tagged("too_large", message).with_hint(&hint)
}
pub(crate) fn compile_timed_out(limit: Duration) -> LoadError {
let hint = format!(
"The compiler cannot be interrupted, so aless stopped waiting for it.\nPass a \
larger --timeout to let it finish, or --timeout 0 for no limit. {}",
Self::SLOW_GRAMMARS
);
Self::compile_late(limit).with_hint(&hint)
}
pub(crate) fn compile_finished_late(limit: Duration, took: Duration) -> LoadError {
let took = Duration::from_millis(timeout_ms(Some(took)));
let hint = format!(
"No thread could be started for the compiler, so it ran to its end on aless's \
own and took {} s.\nPass a larger --timeout to have the grammar, or --timeout 0 \
for no limit. {}",
seconds(took),
Self::SLOW_GRAMMARS
);
Self::compile_late(limit).with_hint(&hint)
}
const SLOW_GRAMMARS: &'static str = "Long repetitions (1*500word), and rules of several \
alternatives that start with one another, make a \
grammar slow to compile.";
fn compile_late(limit: Duration) -> LoadError {
let message = format!(
"timeout: the grammar took longer than {} s to compile",
seconds(limit)
);
LoadError::tagged("timeout", message)
}
fn finished_late(limit: Duration, took: Duration) -> LoadError {
let took = Duration::from_millis(timeout_ms(Some(took)));
let message = format!("timeout: the parse ran longer than {} s", seconds(limit));
let hint = format!(
"The parse finished, but it took {} s.\nPass a larger --timeout to have its \
result, or --timeout 0 for no limit.",
seconds(took)
);
LoadError::tagged("timeout", message).with_hint(&hint)
}
pub(crate) fn with_hint(mut self, hint: &str) -> LoadError {
let hint = escape_controls_but_newlines(hint);
self.report.push_str("\n\n");
for line in wrap(&hint, 72) {
self.report.push_str(" ");
self.report.push_str(&line);
self.report.push('\n');
}
self.report.pop();
self.hint = hint.into();
self
}
pub fn with_origin(mut self, origin: &str) -> LoadError {
self.report = replace_after_header(&self.report, NO_FILE, &escape_controls(origin));
self
}
pub fn from_tabnas(e: &tabnas::TabnasError, hint_template: Option<&str>) -> LoadError {
let mut shown = e.clone();
shown.detail = escape_controls(&e.detail);
shown.hint = escape_hint(&e.hint, hint_template, &e.src);
if e.code == "unexpected" && e.token.name == END_TOKEN && e.src.is_empty() {
shown.detail = "unexpected end of input".to_string();
shown.hint = END_HINT.to_string();
}
shape_excerpt(&mut shown, e);
let detail = shown.detail.trim();
let message = if detail.starts_with(&e.code) || e.code.is_empty() {
detail.to_string()
} else {
format!("{}: {}", e.code, detail)
};
let mut report = shown.to_string();
if shown.col != e.col {
report = replace_after_header(
&report,
&format!("{NO_FILE}:{}:{}", e.row, shown.col),
&format!("{NO_FILE}:{}:{}", e.row, e.col),
);
}
let source_line = e.row.checked_sub(1).and_then(|i| {
e.full_source
.split('\n')
.nth(i)
.map(|l| window(l.strip_suffix('\r').unwrap_or(l), e.col).into())
});
LoadError {
message,
code: if e.code.is_empty() {
"unknown".to_string()
} else {
e.code.clone()
},
line: e.row as u32,
col: e.col as u32,
hint: shown.hint.trim().into(),
source_line,
report,
}
}
pub fn plain_report(&self) -> String {
strip_ansi(&self.report)
}
}
fn room_for(size: Option<u64>, limit: u64) -> u64 {
match size {
Some(n) => {
let mut room = limit.saturating_mul(2);
while room < n {
room = room.saturating_mul(2);
}
room
}
None => limit.saturating_mul(4),
}
}
fn wrap(text: &str, width: usize) -> Vec<String> {
let mut out = Vec::new();
for para in text.split('\n') {
let mut line = String::new();
for word in para.split(' ') {
if !line.is_empty() && line.chars().count() + 1 + word.chars().count() > width {
out.push(std::mem::take(&mut line));
}
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
}
out.push(line);
}
out
}
pub fn escape_controls(s: &str) -> String {
escape(s, false)
}
fn escape_controls_but_newlines(s: &str) -> String {
escape(s, true)
}
pub fn hint_template<'a>(hints: &'a HashMap<String, String>, code: &str) -> Option<&'a str> {
hints
.get(code)
.filter(|t| !t.is_empty())
.or_else(|| hints.get("unknown").filter(|t| !t.is_empty()))
.map(String::as_str)
}
fn escape_hint(hint: &str, template: Option<&str>, src: &str) -> String {
if let Some(escaped) = template.and_then(|t| escape_placeholders(hint, t)) {
return escaped;
}
let hint = if src.chars().any(char::is_control) {
hint.replacen(src, &escape_controls(src), 1)
} else {
hint.to_string()
};
escape_controls_but_newlines(&hint)
}
fn escape_placeholders(hint: &str, template: &str) -> Option<String> {
let mut pattern = String::from("(?s)^");
let mut rest = template.trim();
while let Some(open) = rest.find('{') {
pattern.push_str(®ex::escape(&rest[..open]));
let close = rest[open..].find('}')? + open;
pattern.push_str("(.*?)");
rest = &rest[close + 1..];
}
pattern.push_str(®ex::escape(rest));
pattern.push('$');
let caps = regex::Regex::new(&pattern).ok()?.captures(hint)?;
let mut out = String::with_capacity(hint.len());
let mut last = 0;
for m in caps.iter().skip(1).flatten() {
out.push_str(&hint[last..m.start()]);
out.push_str(&escape_controls(m.as_str()));
last = m.end();
}
out.push_str(&hint[last..]);
Some(out)
}
fn escape(s: &str, keep_newlines: bool) -> String {
if !s.chars().any(char::is_control) {
return s.to_string();
}
let mut out = String::with_capacity(s.len() + 8);
for c in s.chars() {
match c {
'\n' if keep_newlines => out.push('\n'),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
fn replace_after_header(report: &str, from: &str, to: &str) -> String {
match report.find('\n') {
Some(i) => format!("{}{}", &report[..=i], report[i + 1..].replacen(from, to, 1)),
None => report.replacen(from, to, 1),
}
}
const LONG_LINE: usize = 160;
const WINDOW: usize = 120;
const WINDOW_LEAD: usize = 40;
fn window(line: &str, col: usize) -> String {
let chars: Vec<char> = line.chars().collect();
if chars.len() <= LONG_LINE {
return line.to_string();
}
let start = col.max(1).saturating_sub(1).saturating_sub(WINDOW_LEAD);
let end = (start + WINDOW).min(chars.len());
let mut out = String::new();
if start > 0 {
out.push('…');
}
out.extend(&chars[start..end]);
if end < chars.len() {
out.push('…');
}
out
}
fn shape_excerpt(shown: &mut tabnas::TabnasError, e: &tabnas::TabnasError) {
if e.full_source.is_empty() {
return;
}
let lines: Vec<&str> = e.full_source.split('\n').collect();
let at = e.row.max(1).saturating_sub(1).min(lines.len() - 1);
let lo = at.saturating_sub(2);
let hi = (at + 2).min(lines.len() - 1);
let quoted: Vec<Vec<char>> = lines[lo..=hi]
.iter()
.map(|l| l.strip_suffix('\r').unwrap_or(l).chars().collect())
.collect();
let c0 = e.col.max(1) - 1;
let long = quoted.iter().any(|l| l.len() > LONG_LINE);
let (start, end) = if long {
let start = c0.saturating_sub(WINDOW_LEAD);
(start, start + WINDOW)
} else {
(0, usize::MAX)
};
let mut src = "\n".repeat(lo);
for (i, line) in quoted.iter().enumerate() {
if i > 0 {
src.push('\n');
}
if start > 0 && !line.is_empty() {
src.push('…');
}
src.extend(
line.iter()
.skip(start)
.take(end - start)
.map(|&c| visible(c)),
);
if line.len() > end {
src.push('…');
}
}
shown.full_source = src;
shown.col = c0 - start + 1 + usize::from(start > 0);
let rest_of_line = quoted[at - lo].len().saturating_sub(c0);
let carets = e.src.chars().count().min(rest_of_line).min(end - c0).max(1);
shown.src = e.src.chars().take(carets).collect();
}
fn visible(c: char) -> char {
match c {
'\t' => '\t',
'\u{0}'..='\u{1f}' => char::from_u32(0x2400 + c as u32).unwrap_or('\u{fffd}'),
'\u{7f}' => '\u{2421}',
c if c.is_control() => '\u{fffd}',
c => c,
}
}
pub fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c != '\x1b' {
out.push(c);
continue;
}
match chars.next() {
Some('[') => {
for n in chars.by_ref() {
if ('\x40'..='\x7e').contains(&n) {
break;
}
}
}
Some(']') => {
while let Some(n) = chars.next() {
if n == '\x07' {
break;
}
if n == '\x1b' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
_ => {}
}
}
out
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.line > 0 {
write!(f, "{}:{}: {}", self.line, self.col, self.message)
} else {
f.write_str(&self.message)
}
}
}
impl std::error::Error for LoadError {}
#[derive(Clone, Debug)]
pub struct Loaded {
pub doc: Doc,
pub format: Format,
pub source: String,
}
pub(crate) fn make_parser(format: Format) -> Result<Option<Tabnas>, LoadError> {
let mut parser = match format {
Format::Json => tabnas_json::make(),
Format::Jsonl => tabnas_jsonl::make(),
Format::Jsonic => tabnas_jsonic::make(),
Format::Jsonc => tabnas_jsonc::make_with(
tabnas_jsonc::JsoncOptions::new().with_allow_trailing_comma(true),
),
Format::Json5 => tabnas_json5::make(),
Format::Yaml => tabnas_yaml::make(),
Format::Toml => tabnas_toml::make(),
Format::Ini => tabnas_ini::make(),
Format::Csv => tabnas_csv::make(),
Format::Tsv => {
let mut options = tabnas_csv::CsvOptions::default();
options.field.separation = Some("\t".to_string());
tabnas_csv::make_with(options)
}
Format::Xml => tabnas_xml::make(),
Format::Zon => tabnas_zon::make(),
Format::Markdown => tabnas_markdown::make(),
Format::Feed => tabnas_feed::make(),
Format::Css => tabnas_css::make(),
Format::Proto => tabnas_proto::make(),
Format::Pgn => match tabnas_chess::make(&tabnas_chess::ChessOptions::default()) {
Ok(mut parser) => {
parser.options.color.active = tabnas::Options::default().color.active;
parser
}
Err(e) => return Err(grammar_failed(format, &e.to_string())),
},
Format::Expr => tabnas_expr::make(),
Format::Semver => tabnas_semver::make(),
Format::Text => return Ok(None),
Format::Custom(id) => {
let Some(g) = grammar::get(id) else {
return Err(grammar_failed(format, "the grammar is not registered"));
};
match catch_grammar(|| g.parser()) {
Ok(Ok(parser)) => parser,
Ok(Err(e)) | Err(e) => return Err(grammar_failed(format, &e)),
}
}
};
parser.options.rule.history.get_or_insert(RULE_HISTORY);
Ok(Some(parser))
}
pub(crate) fn parser_text(src: &str) -> &str {
src.strip_prefix('\u{feff}').unwrap_or(src)
}
pub(crate) fn parse_text(src: &str, format: Format) -> &str {
let text = parser_text(src);
match format {
Format::Semver => text
.strip_suffix("\r\n")
.or_else(|| text.strip_suffix('\n'))
.unwrap_or(text),
_ => text,
}
}
pub(crate) fn check_text(format: Format, src: &str) -> Result<(), LoadError> {
match format {
Format::Proto => tabnas_proto::preflight(src).map_err(|e| proto_refused(format, e)),
_ => Ok(()),
}
}
pub(crate) fn read_as(format: Format, value: tabnas::Value) -> Result<tabnas::Value, LoadError> {
match format {
Format::Proto => {
let descriptor =
tabnas_proto::to_descriptor(&value, None).map_err(|e| proto_refused(format, e))?;
drop(value);
let json = serde_json::to_value(&descriptor)
.map_err(|e| grammar_failed(format, &e.to_string()))?;
Ok(tabnas::Value::from_json(&json))
}
Format::Expr => Ok(tabnas_expr::simplify(&value)),
_ => Ok(value),
}
}
fn proto_refused(format: Format, e: tabnas_proto::ProtoError) -> LoadError {
use tabnas_proto::ProtoError;
match e {
ProtoError::TooDeep(_) => {
let (message, hint) =
too_deep_words(format, Deep::Braces(tabnas_proto::MAX_NESTING_DEPTH));
LoadError::tagged("too_deep", message).with_hint(&hint)
}
ProtoError::Version(message) => {
let detail = message.strip_prefix("proto: ").unwrap_or(&message);
LoadError::refused(
format,
"version",
format!("version: {detail}"),
"A .proto file declares its version as syntax = \"proto2\" or \"proto3\", or \
edition = \"2023\" or \"2024\"; one that declares none is read as proto2.",
)
}
ProtoError::Parse(e) => LoadError::from_tabnas(&e, None),
ProtoError::Grammar(message) => grammar_failed(format, &message),
}
}
pub fn lines(src: &str) -> Vec<&str> {
let mut out: Vec<&str> = src
.split('\n')
.map(|l| l.strip_suffix('\r').unwrap_or(l))
.collect();
if out.last() == Some(&"") {
out.pop();
}
out
}
pub fn line_count(src: &str) -> usize {
let breaks = src.bytes().filter(|&b| b == b'\n').count();
if src.is_empty() || src.ends_with('\n') {
breaks
} else {
breaks + 1
}
}
thread_local! {
static CATCHING: Cell<usize> = const { Cell::new(0) };
}
pub fn parse_in_progress() -> bool {
CATCHING.try_with(|c| c.get() > 0).unwrap_or(false)
}
pub(crate) struct CatchGuard;
impl CatchGuard {
pub(crate) fn enter() -> CatchGuard {
CATCHING.with(|c| c.set(c.get() + 1));
CatchGuard
}
}
impl Drop for CatchGuard {
fn drop(&mut self) {
let _ = CATCHING.try_with(|c| c.set(c.get() - 1));
}
}
pub(crate) fn catch_grammar<T>(work: impl FnOnce() -> T) -> Result<T, String> {
let _guard = CatchGuard::enter();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(work))
.map_err(|panic| panic_message(&*panic))
}
fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
panic
.downcast_ref::<String>()
.cloned()
.or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_else(|| "unknown panic".to_string())
}
pub(crate) fn grammar_failed(format: Format, what: &str) -> LoadError {
LoadError::tagged("grammar", format!("{format} grammar failed: {what}"))
}
pub fn parse(src: &str, format: Format) -> Result<Doc, LoadError> {
parse_within(src, format, Limits::current().timeout)
}
pub fn parse_within(
src: &str,
format: Format,
timeout: Option<Duration>,
) -> Result<Doc, LoadError> {
parse_until(src, format, timeout, None)
}
pub(crate) fn parse_until(
src: &str,
format: Format,
timeout: Option<Duration>,
started: Option<Instant>,
) -> Result<Doc, LoadError> {
if format == Format::Text {
let src = src.strip_prefix('\u{feff}').unwrap_or(src);
return Ok(Doc::from_lines(&lines(src)));
}
let src = parse_text(src, format);
on_parse_thread(
timeout,
started,
|| {},
|deadline| parse_here(src, format, deadline),
)
}
pub(crate) fn on_parse_thread<T: Send>(
timeout: Option<Duration>,
started: Option<Instant>,
on_alarm: impl FnOnce() + Send,
work: impl FnOnce(Option<Deadline>) -> T + Send,
) -> T {
let deadline = timeout.and_then(|limit| {
let at = started.unwrap_or_else(Instant::now).checked_add(limit)?;
Some(Deadline {
at,
limit,
alarm: None,
cancel: None,
})
});
let alarm = Arc::new(AtomicBool::new(false));
let watched = deadline.clone().map(|d| Deadline {
alarm: Some(alarm.clone()),
..d
});
let (done, finished) = mpsc::channel::<()>();
let slot = std::sync::Mutex::new(Some(work));
let take = || slot.lock().unwrap_or_else(|e| e.into_inner()).take();
std::thread::scope(|scope| {
let spawned = std::thread::Builder::new()
.name("aless-parse".into())
.stack_size(PARSE_STACK)
.spawn_scoped(scope, || {
let _done = done;
let work = take().expect("the work runs once");
work(watched)
});
match spawned {
Ok(handle) => {
if let Some(d) = &deadline {
let left = d.at.saturating_duration_since(Instant::now());
if finished.recv_timeout(left) == Err(RecvTimeoutError::Timeout) {
alarm.store(true, Ordering::Relaxed);
on_alarm();
}
}
handle
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
}
Err(_) => {
let work = take().expect("a failed spawn never ran the work");
work(deadline)
}
}
})
}
fn parse_here(src: &str, format: Format, deadline: Option<Deadline>) -> Result<Doc, LoadError> {
let Some(parser) = make_parser(format)? else {
return Ok(Doc::from_lines(&lines(src)));
};
parse_with(parser, src, format, deadline)
}
fn parse_with(
parser: Tabnas,
src: &str,
format: Format,
deadline: Option<Deadline>,
) -> Result<Doc, LoadError> {
parse_capped(parser, src, format, deadline, MAX_RULE_DEPTH)
}
fn parse_capped(
mut parser: Tabnas,
src: &str,
format: Format,
deadline: Option<Deadline>,
max_depth: usize,
) -> Result<Doc, LoadError> {
let stopped = guard(&mut parser, deadline.clone(), max_depth, || {});
parse_guarded(parser, &stopped, src, format, deadline, max_depth)
}
fn parse_guarded(
mut parser: Tabnas,
stopped: &Stop,
src: &str,
format: Format,
deadline: Option<Deadline>,
max_depth: usize,
) -> Result<Doc, LoadError> {
check_text(format, src)?;
let sink = if format.is_custom() {
prov::capture_words(&mut parser)
} else if format.keyed_by_header() {
prov::capture_cells(&mut parser)
} else {
prov::capture(&mut parser)
};
let outcome = {
let _guard = CatchGuard::enter();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
parser.parse(src).map_err(Box::new)
}))
};
let stop = stopped.why();
let now = Instant::now();
let late = deadline
.as_ref()
.filter(|d| stop == 0 && outcome.is_ok() && now >= d.at);
if let Some(d) = late {
let took = d.limit + now.saturating_duration_since(d.at);
return Err(LoadError::finished_late(d.limit, took));
}
match outcome {
Ok(Ok(value)) => {
drop(parser);
if format.is_custom() && nests_past(&value, MAX_VALUE_DEPTH) {
drop_deep(value);
let (message, hint) = too_deep_words(format, Deep::Value);
return Err(LoadError::tagged("too_deep", message).with_hint(&hint));
}
let value = read_as(format, value)?;
let mut doc = Doc::from_value(&value);
drop(value);
if let Ok(toks) = sink.lock() {
if format.keyed_by_header() {
prov::align_records(&mut doc, &toks);
} else {
prov::align(&mut doc, &toks);
}
}
Ok(doc)
}
Ok(Err(e)) if stop == STOP_DEPTH => {
let mut e = *e;
stopped.place(&mut e);
e.tag = "aless".to_string();
e.code = "too_deep".to_string();
(e.detail, e.hint) = too_deep_words(format, Deep::Rules(max_depth));
Err(LoadError::from_tabnas(&e, None))
}
Ok(Err(e)) if stop == STOP_TIME => {
let mut e = *e;
stopped.place(&mut e);
let limit = seconds(deadline.map_or(Duration::ZERO, |d| d.limit));
e.tag = "aless".to_string();
e.code = "timeout".to_string();
e.detail = format!("timeout: the parse ran longer than {limit} s");
e.hint = format!(
"The parse had got this far when --timeout {limit} stopped it.\nPass a \
larger --timeout to let it finish, or --timeout 0 for no limit."
);
Err(LoadError::from_tabnas(&e, None))
}
Ok(Err(e)) if e.code == "cancel" => {
let mut e = *e;
e.tag = "aless".to_string();
e.code = "too_deep".to_string();
(e.detail, e.hint) = too_deep_words(format, Deep::Grammar);
Err(LoadError::from_tabnas(&e, None))
}
Ok(Err(e)) => {
let hints = parser.config().hint;
Err(LoadError::from_tabnas(&e, hint_template(&hints, &e.code)))
}
Err(panic) => Err(grammar_failed(format, &panic_message(&*panic))),
}
}
pub(crate) fn guard(
parser: &mut Tabnas,
deadline: Option<Deadline>,
max_depth: usize,
notify: impl Fn() + Send + Sync + 'static,
) -> Arc<Stop> {
let stopped = Stop::new();
let flag = stopped.clone();
let own = parser.config().parse.budget;
let (every, check) = (own.check_every_n, own.on_check);
parser.parse_budget(1, move |ctx| {
if ctx.rule_stack.len() > max_depth {
flag.record(STOP_DEPTH, ctx);
notify();
return false;
}
if deadline.as_ref().is_some_and(Deadline::passed) {
flag.record(STOP_TIME, ctx);
notify();
return false;
}
match &check {
Some(check) if every > 0 && ctx.iteration % every == 0 => check(ctx),
_ => true,
}
});
stopped
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Deep {
Rules(usize),
Value,
Grammar,
Braces(usize),
}
pub(crate) fn too_deep_words(format: Format, why: Deep) -> (String, String) {
match why {
Deep::Rules(cap) if format.is_custom() => (
format!("too_deep: the {format} grammar had {cap} rules open at once"),
"Rule depth follows the nesting of the input and of the grammar's own recursion, \
not the length of a file: a repetition adds none. The parse stopped here.\nReal \
documents nest a few dozen levels at most."
.to_string(),
),
Deep::Rules(cap) => (
format!(
"too_deep: nested deeper than aless reads (about {} levels)",
cap / 3
),
"Nesting this deep would overflow the parser's stack, so the parse stopped \
here.\nReal documents nest a few dozen levels at most."
.to_string(),
),
Deep::Value => (
format!(
"too_deep: the {format} grammar built a value nested deeper than aless reads \
(about {MAX_VALUE_DEPTH} levels)"
),
"The value is measured once the parse is done: a grammar from the command line \
builds it in the compiler's shape, which its rule stack does not count in the \
same units.\nReal documents nest a few dozen levels at most."
.to_string(),
),
Deep::Grammar if format == Format::Expr => (
"too_deep: an expression larger or deeper than the expr grammar reads".to_string(),
format!(
"The expr grammar holds at most {limit} operations in one expression, a group \
in parentheses among them, and nests lists and maps at most {limit} levels \
deep; the parse stopped where the document passed one of them.\nSplit a long \
expression into several.",
limit = tabnas_expr::NODE_LIMIT
),
),
Deep::Grammar => (
format!("too_deep: nested deeper than the {format} grammar reads"),
format!(
"The {format} grammar has a nesting limit of its own, and the parse stopped \
where the document passed it.\nReal documents nest a few dozen levels at most."
),
),
Deep::Braces(levels) => (
format!("too_deep: nested deeper than the {format} grammar reads"),
format!(
"The {format} reader counts a document's braces before it parses it, and \
refuses one that nests them more than {levels} levels deep.\nReal documents \
nest a few levels at most."
),
),
}
}
pub(crate) fn nests_past(value: &tabnas::Value, cap: usize) -> bool {
use tabnas::Value;
let mut stack = vec![(value, 1usize)];
while let Some((v, depth)) = stack.pop() {
if depth > cap {
return true;
}
match v {
Value::Array(items) => stack.extend(items.iter().map(|c| (c, depth + 1))),
Value::ListRef(list) => {
stack.extend(list.value.iter().map(|c| (c, depth + 1)));
if let Some(child) = &list.child {
stack.push((child, depth + 1));
}
}
Value::Object(map) => stack.extend(map.values().map(|c| (c, depth + 1))),
Value::MapRef(map) => stack.extend(map.value.values().map(|c| (c, depth + 1))),
_ => {}
}
}
false
}
pub(crate) fn drop_deep(value: tabnas::Value) {
use tabnas::Value;
let mut queue = vec![value];
while let Some(v) = queue.pop() {
match v {
Value::Array(items) => {
if let Ok(items) = Arc::try_unwrap(items) {
queue.extend(items);
}
}
Value::ListRef(list) => {
if let Ok(list) = Arc::try_unwrap(list) {
queue.extend(list.value);
if let Some(child) = list.child {
queue.push(*child);
}
}
}
Value::Object(map) => {
if let Ok(map) = Arc::try_unwrap(map) {
queue.extend(map.into_values());
}
}
Value::MapRef(map) => {
if let Ok(map) = Arc::try_unwrap(map) {
queue.extend(map.value.into_values());
}
}
_ => {}
}
}
}
pub fn load_str(source: String, format: Format) -> Result<Loaded, LoadError> {
load_str_within(source, format, Limits::current())
}
pub fn load_str_within(
source: String,
format: Format,
limits: Limits,
) -> Result<Loaded, LoadError> {
let doc = parse_until(&source, format, limits.timeout, limits.started)?;
Ok(Loaded {
doc,
format,
source,
})
}
pub fn origin_of(path: &Path) -> String {
std::env::current_dir()
.ok()
.and_then(|cwd| path.strip_prefix(cwd).ok().map(Path::to_path_buf))
.filter(|rel| !rel.as_os_str().is_empty())
.unwrap_or_else(|| path.to_path_buf())
.display()
.to_string()
}
pub fn load_path(path: &Path, format: Option<Format>) -> Result<Loaded, LoadError> {
load_path_within(path, format, Limits::current())
}
pub fn load_path_within(
path: &Path,
format: Option<Format>,
limits: Limits,
) -> Result<Loaded, LoadError> {
let source = read_path_within(path, limits.max_size)?;
let format = format.unwrap_or_else(|| Format::detect(path));
load_str_within(source, format, limits).map_err(|e| e.with_origin(&origin_of(path)))
}
pub fn read_path_within(path: &Path, max_size: Option<u64>) -> Result<String, LoadError> {
let origin = origin_of(path);
let file = std::fs::File::open(path)
.map_err(|e| LoadError::new(e.to_string()).with_origin(&origin))?;
if let (Some(max), Ok(meta)) = (max_size, file.metadata()) {
if meta.is_file() && meta.len() > max {
return Err(LoadError::too_large(Some(meta.len()), max).with_origin(&origin));
}
}
let bytes = read_within(file, max_size).map_err(|e| e.with_origin(&origin))?;
Ok(match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::doc::{Key, Kind};
#[test]
fn catch_guard_marks_the_parse() {
assert!(!parse_in_progress());
{
let _g = CatchGuard::enter();
assert!(parse_in_progress());
let elsewhere = std::thread::spawn(parse_in_progress).join().unwrap();
assert!(!elsewhere);
}
assert!(!parse_in_progress());
}
#[test]
fn a_grammar_panic_is_caught_as_its_message() {
assert_eq!(catch_grammar(|| 1 + 1), Ok(2));
assert_eq!(catch_grammar(|| panic!("boom")).unwrap_err(), "boom");
let n = 3;
assert_eq!(
catch_grammar(|| -> () { panic!("bad {n}") }).unwrap_err(),
"bad 3"
);
assert!(
!parse_in_progress(),
"the guard is left when the panic is caught"
);
let e = grammar_failed(Format::Json, "boom");
assert_eq!(e.code, "grammar");
assert_eq!(e.message, "json grammar failed: boom");
assert!(!e.is_io() && !e.is_timeout() && !e.is_too_large());
}
#[test]
fn detection() {
assert_eq!(Format::detect(Path::new("a/b.json")), Format::Json);
assert_eq!(Format::detect(Path::new("x.YML")), Format::Yaml);
assert_eq!(Format::detect(Path::new("notes")), Format::Text);
assert_eq!(Format::detect(Path::new("weird.xyz")), Format::Text);
assert_eq!(Format::detect_known(Path::new("weird.xyz")), None);
assert_eq!(
Format::detect_known(Path::new("a.toml")),
Some(Format::Toml)
);
assert_eq!(Format::from_name("md"), Some(Format::Markdown));
assert_eq!(Format::from_name("Markdown"), Some(Format::Markdown));
assert_eq!(Format::from_name("nope"), None);
assert_eq!(Format::from_name("ndjson"), Some(Format::Jsonl));
assert!(Format::known_names().starts_with(&["json", "jsonl"]));
assert!(!Format::Json.is_custom());
assert_eq!(Format::detect(Path::new("site.css")), Format::Css);
assert_eq!(Format::detect(Path::new("api/v1.proto")), Format::Proto);
assert_eq!(Format::detect(Path::new("games.PGN")), Format::Pgn);
assert!(Format::Expr.extensions().is_empty() && Format::Semver.extensions().is_empty());
assert_eq!(Format::detect(Path::new("calc.expr")), Format::Text);
assert_eq!(Format::detect(Path::new("VERSION")), Format::Text);
assert_eq!(Format::from_name("expr"), Some(Format::Expr));
assert_eq!(Format::from_name("semver"), Some(Format::Semver));
assert_eq!(Format::from_name("pgn"), Some(Format::Pgn));
assert_eq!(
Format::from_name("chess"),
None,
"PGN goes by its languageId"
);
}
#[test]
fn custom_grammars_are_detected_by_file_name_or_extension() {
let def = grammar::Definition::parse(
"--grammar-expr",
"impl-etc-hosts,impl-etc-hosts.conf=doc = *entry\nentry = TX \"=\" TX\n",
)
.unwrap();
let id = grammar::register(def, Limits::NONE).unwrap();
let custom = Format::Custom(id);
for path in [
"/etc/impl-etc-hosts",
"impl-etc-hosts",
"./IMPL-ETC-HOSTS",
"backup/hosts.impl-etc-hosts",
"/etc/impl-etc-hosts.conf",
"Impl-Etc-Hosts.Conf",
] {
assert_eq!(Format::detect(Path::new(path)), custom, "{path}");
assert_eq!(
Format::detect_known(Path::new(path)),
Some(custom),
"{path}"
);
}
for path in ["impl-etc-hosts.bak", "impl-etc-hosts/x.json", "x.conf"] {
assert_ne!(Format::detect(Path::new(path)), custom, "{path}");
}
assert_eq!(Format::from_name("impl-etc-hosts.conf"), Some(custom));
assert!(custom.is_custom());
assert!(Format::known_names().contains(&"impl-etc-hosts"));
let doc = parse("a: = b:c\nd = e\n", custom).unwrap();
assert!(doc.len() > 1);
let e = parse("a = 1\nb\nc = 3\n", custom).unwrap_err();
assert_eq!((e.line, e.col), (2, 1), "{e}");
assert_eq!(e.code, "unexpected");
}
fn deepest_rule(mut parser: Tabnas, src: &str) -> usize {
let deepest = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let seen = deepest.clone();
parser.parse_budget(1, move |ctx| {
seen.fetch_max(ctx.rule_stack.len(), Ordering::Relaxed);
true
});
parser.parse(src).unwrap_or_else(|e| panic!("{e}"));
deepest.load(Ordering::Relaxed)
}
#[test]
fn a_custom_grammars_repetition_is_not_nesting() {
let def = grammar::Definition::parse(
"--grammar-expr",
&format!("impl-kv-l={}", grammar::tests::KV),
)
.unwrap();
let custom = Format::Custom(grammar::register(def, Limits::NONE).unwrap());
let lines = |n: usize| -> String { (0..n).map(|i| format!("k{i}=v{i}\n")).collect() };
let flat = lines(2_000);
let doc = parse(&flat, custom).unwrap();
assert_eq!(doc.node(0).children, 2_000);
assert_eq!(doc.nodes.iter().map(|n| n.depth).max(), Some(2));
let two = deepest_rule(make_parser(custom).unwrap().unwrap(), &lines(2));
let many = deepest_rule(make_parser(custom).unwrap().unwrap(), &flat);
assert_eq!(
many, two,
"2,000 lines reach rule depth {many} where two lines reach {two}"
);
assert!(two < MAX_RULE_DEPTH / 100, "{two}");
let parser = make_parser(custom).unwrap().unwrap();
let doc = parse_capped(parser, &flat, custom, None, two).unwrap();
assert_eq!(doc.node(0).children, 2_000);
let parser = make_parser(custom).unwrap().unwrap();
let e = parse_capped(parser, &flat, custom, None, two - 1).unwrap_err();
assert_eq!(e.code, "too_deep");
assert_eq!(
e.message,
format!(
"too_deep: the impl-kv-l grammar had {} rules open at once",
two - 1
)
);
assert!(e.hint.contains("a repetition adds none"), "{}", e.hint);
assert!(e.line > 0, "{e}");
assert!(e.plain_report().starts_with("[aless/too_deep]"));
let (message, hint) = too_deep_words(Format::Yaml, Deep::Rules(MAX_RULE_DEPTH));
assert_eq!(
message,
"too_deep: nested deeper than aless reads (about 1000 levels)"
);
assert!(hint.contains("overflow the parser's stack"), "{hint}");
}
#[test]
fn a_ten_thousand_line_hosts_file_parses_under_the_shared_cap() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/grammars/hosts.abnf"
);
let def = grammar::Definition::parse("--grammar", &format!("hosts-l={path}")).unwrap();
let custom = Format::Custom(grammar::register(def, Limits::NONE).unwrap());
let lines = |n: usize| -> String {
(0..n)
.map(|i| {
format!(
"10.0.{}.{}\thost{i}.example.com alias{i}\n",
(i >> 8) & 255,
i & 255
)
})
.collect()
};
let doc = parse(&lines(10_000), custom).unwrap();
assert_eq!(doc.node(0).children, 10_000);
let three = deepest_rule(make_parser(custom).unwrap().unwrap(), &lines(3));
let many = deepest_rule(make_parser(custom).unwrap().unwrap(), &lines(10_000));
assert_eq!(
many, three,
"10,000 lines reach rule depth {many} where three lines reach {three}"
);
assert!(three < MAX_RULE_DEPTH / 100, "{three}");
}
#[test]
fn a_custom_value_nested_past_the_cap_is_too_deep() {
let def = grammar::Definition::parse(
"--grammar-expr",
"impl-nest-l=doc = \"(\" doc \")\" / \"x\"\n",
)
.unwrap();
let custom = Format::Custom(grammar::register(def, Limits::NONE).unwrap());
let parens = |n: usize| "(".repeat(n) + "x" + &")".repeat(n);
let doc = parse(&parens(400), custom).unwrap();
let deepest = doc.nodes.iter().map(|n| n.depth).max().unwrap();
assert!((700..1000).contains(&deepest), "{deepest}");
let e = parse(&parens(600), custom).unwrap_err();
assert_eq!(e.code, "too_deep");
assert_eq!(
e.message,
"too_deep: the impl-nest-l grammar built a value nested deeper than aless reads \
(about 1000 levels)"
);
assert_eq!(
(e.line, e.col),
(0, 0),
"found after the parse: no position"
);
assert!(e.plain_report().starts_with("[aless/too_deep]"));
}
#[test]
fn deep_values_are_measured_and_dropped_without_recursion() {
use tabnas::Value;
assert!(!nests_past(&Value::Null, 1));
assert!(nests_past(&Value::Array(Arc::new(vec![Value::Null])), 1));
let mut v = Value::String("x".into());
for i in 0..200_000 {
v = if i % 2 == 0 {
Value::Array(Arc::new(vec![v]))
} else {
Value::Object(Arc::new(std::iter::once(("k".to_string(), v)).collect()))
};
}
assert!(nests_past(&v, MAX_VALUE_DEPTH));
assert!(!nests_past(&v, 300_000));
drop_deep(v);
}
#[test]
fn a_lists_child_counts_toward_its_depth() {
use tabnas::{ListRef, Value};
let mut v = Value::Null;
for _ in 0..(MAX_VALUE_DEPTH + 10) {
v = Value::ListRef(Arc::new(ListRef {
value: Vec::new(),
implicit: false,
child: Some(Box::new(v)),
meta: Default::default(),
}));
}
assert!(nests_past(&v, MAX_VALUE_DEPTH));
assert!(!nests_past(&v, MAX_VALUE_DEPTH + 20));
drop_deep(v);
}
#[test]
fn text_lines() {
assert_eq!(lines("a\r\nb\n\nc\n"), vec!["a", "b", "", "c"]);
assert_eq!(lines(""), Vec::<&str>::new());
assert_eq!(lines("x"), vec!["x"]);
for text in [
"",
"x",
"x\n",
"\n",
"\n\n",
"a\r\nb",
"a\r\nb\r\n",
"a\r",
"a\n\nb\n",
] {
assert_eq!(line_count(text), lines(text).len(), "{text:?}");
}
let d = parse("one\ntwo\n", Format::Text).unwrap();
assert_eq!(d.len(), 3);
assert_eq!(d.node(2).kind, Kind::Str("two".into()));
assert_eq!(d.node(2).line, 2);
}
#[test]
fn sizes_parse_as_max_size_takes_them() {
assert_eq!(parse_size("64M"), Ok(Some(64 << 20)));
assert_eq!(parse_size("64mib"), Ok(Some(64 << 20)));
assert_eq!(parse_size("1G"), Ok(Some(1 << 30)));
assert_eq!(parse_size("512k"), Ok(Some(512 << 10)));
assert_eq!(parse_size(" 1000 "), Ok(Some(1000)));
assert_eq!(parse_size("0"), Ok(None));
assert_eq!(parse_size("0M"), Ok(None));
for bad in ["", "M", "1T", "-1", "1.5M", "lots", "99999999999999999999G"] {
assert!(parse_size(bad).is_err(), "{bad:?}");
}
assert_eq!(size_flag(64 << 20), "64M");
assert_eq!(size_flag(1 << 30), "1G");
assert_eq!(size_flag(1536), "1536");
assert_eq!(size_flag(3 << 10), "3K");
}
#[test]
fn reading_stops_at_the_limit() {
assert_eq!(read_within(&b"abcd"[..], Some(4)).unwrap(), b"abcd");
assert_eq!(read_within(&b"abcd"[..], None).unwrap(), b"abcd");
let e = read_within(&b"abcde"[..], Some(4)).unwrap_err();
assert!(e.is_too_large());
assert_eq!(e.message, "input is over the 4 B limit");
assert!(e.hint.contains("such as 16,"), "{}", e.hint);
let e = read_within(std::io::repeat(b'x'), Some(1 << 20)).unwrap_err();
assert!(e.is_too_large());
}
fn sized(max_size: Option<u64>) -> Limits {
Limits {
max_size,
timeout: None,
started: None,
}
}
#[test]
fn a_file_over_the_limit_is_refused_unread() {
let dir = std::env::temp_dir().join(format!("aless-size-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let big = dir.join("big.json");
std::fs::write(&big, format!("[{}1]", "1, ".repeat(2000))).unwrap();
let e = load_path_within(&big, None, sized(Some(1024))).unwrap_err();
assert!(e.is_too_large());
assert_eq!(e.message, "input is 5.9 KB, over the 1.0 KB limit");
assert!(
e.hint.contains("Pass --max-size 8K (or more)"),
"{}",
e.hint
);
let report = e.plain_report();
assert!(
report.starts_with("[aless/too_large]: input is 5.9 KB"),
"{report}"
);
assert!(report.contains("big.json"), "{report}");
assert!(load_path_within(&big, None, sized(Some(1 << 20))).is_ok());
assert!(load_path_within(&big, None, sized(None)).is_ok());
std::fs::remove_dir_all(&dir).unwrap();
}
fn many_tables(n: usize) -> String {
(0..n)
.map(|i| format!("[[item]]\nid = {i}\nname = \"item {i}\"\n\n"))
.collect()
}
#[test]
fn timeouts_parse_as_the_option_takes_them() {
let secs = |s: f64| Ok(Some(Duration::from_secs_f64(s)));
assert_eq!(parse_timeout("30"), secs(30.0));
assert_eq!(parse_timeout("2.5"), secs(2.5));
assert_eq!(parse_timeout("2.5s"), secs(2.5));
assert_eq!(parse_timeout(" 1.5m "), secs(90.0));
assert_eq!(parse_timeout("0"), Ok(None));
assert_eq!(parse_timeout("0s"), Ok(None));
for bad in ["", "s", "-1", "inf", "NaN", "10ms", "soon", "1e400"] {
assert!(parse_timeout(bad).is_err(), "{bad:?}");
}
assert_eq!(seconds(Duration::from_millis(2500)), "2.5");
assert_eq!(seconds(Duration::from_secs(30)), "30");
}
#[test]
fn the_process_limits_default_and_encode() {
assert_eq!(Limits::current(), Limits::DEFAULT);
assert_eq!(Limits::default(), Limits::DEFAULT);
assert_eq!(timeout_ms(None), 0);
assert_eq!(timeout_ms(Some(Duration::from_millis(1500))), 1500);
assert_eq!(timeout_ms(Some(Duration::from_nanos(1))), 1);
}
#[test]
fn a_parse_past_its_timeout_stops_where_it_got_to() {
let src = many_tables(2_000);
let e = parse_within(&src, Format::Toml, Some(Duration::from_millis(1))).unwrap_err();
assert!(e.is_timeout(), "{}", e.message);
assert_eq!(e.message, "timeout: the parse ran longer than 0.001 s");
assert!(e.line > 0, "the report shows how far the parse got");
assert!(e.hint.contains("--timeout 0 for no limit"), "{}", e.hint);
assert!(e.plain_report().starts_with("[aless/timeout]"));
let quick = parse_within(&many_tables(2), Format::Toml, Some(Duration::from_secs(60)));
assert_eq!(quick.unwrap().len(), 1 + 1 + 2 * 3);
}
fn records(n: usize) -> String {
let items: String = (0..n).map(|i| format!("{{\"n\": {i}}},\n")).collect();
format!("[\n{items}{{}}]\n")
}
fn between_steps_past(parser: &mut Tabnas, line: usize, up: Arc<AtomicBool>) {
let guarded = parser.config().parse.budget;
parser.parse_budget(1, move |ctx| {
if ctx.t.is_empty() && ctx.v1().is_some_and(|t| t.site.ri >= line) {
up.store(true, Ordering::Relaxed);
}
guarded.on_check.as_ref().is_none_or(|check| check(ctx))
});
}
#[test]
fn a_timeout_between_steps_is_placed_at_the_last_token_read() {
let src = records(300);
let mut parser = make_parser(Format::Json).unwrap().unwrap();
let up = Arc::new(AtomicBool::new(false));
let deadline = Deadline::after(Some(Duration::from_secs(600)))
.unwrap()
.or_cancelled(up.clone());
let stopped = guard(&mut parser, Some(deadline.clone()), MAX_RULE_DEPTH, || {});
between_steps_past(&mut parser, 200, up);
let e = parse_guarded(
parser,
&stopped,
&src,
Format::Json,
Some(deadline),
MAX_RULE_DEPTH,
)
.unwrap_err();
assert!(e.is_timeout(), "{}", e.message);
assert!(
(200..=201).contains(&e.line) && e.col > 0,
"stopped at {}:{}",
e.line,
e.col
);
let line = e.source_line.as_deref().unwrap_or_default();
assert!(line.contains("\"n\": 19"), "{line:?}");
assert!(
e.plain_report().contains(&format!(":{}:{}", e.line, e.col)),
"{}",
e.plain_report()
);
assert_eq!(stopped.position(), (e.line, e.col));
}
#[test]
fn too_deep_between_steps_is_placed_at_the_last_token_read() {
let src = format!("{}\n{}", "[\n".repeat(30), "]".repeat(30));
let mut parser = make_parser(Format::Json).unwrap().unwrap();
let stopped = guard(&mut parser, None, 40, || {});
let e = parse_guarded(parser, &stopped, &src, Format::Json, None, 40).unwrap_err();
assert_eq!(e.code, "too_deep", "{}", e.message);
assert!(e.line > 1, "stopped at {}:{}", e.line, e.col);
assert_eq!(stopped.position(), (e.line, e.col));
}
#[test]
fn a_read_past_its_deadline_is_a_timeout() {
let data: Vec<u8> = (0..300_000u32).map(|i| (i % 251) as u8).collect();
let whole = DeadlineReader::new(
std::io::Cursor::new(data.clone()),
Instant::now(),
Duration::from_secs(600),
)
.unwrap();
assert_eq!(read_within(whole, None).unwrap(), data);
for first in [&b""[..], b"[1, 2,"] {
let started = Instant::now();
let reader = DeadlineReader::new(quiet(first), started, Duration::from_millis(200));
let e = read_within(reader.unwrap(), Some(1 << 20)).unwrap_err();
let took = started.elapsed();
assert!(e.is_timeout(), "{}", e.message);
assert_eq!(
e.message,
"timeout: the input was still being read after 0.2 s"
);
assert!(e.hint.contains("--timeout 0 for no limit"), "{}", e.hint);
assert!(e.plain_report().starts_with("[aless/timeout]"));
assert!(
took >= Duration::from_millis(200) && took < Duration::from_secs(10),
"{took:?}"
);
}
}
#[test]
fn an_endless_input_is_a_timeout_too() {
let started = Instant::now();
let endless =
DeadlineReader::new(std::io::repeat(b'x'), started, Duration::from_millis(200));
let e = read_within(endless.unwrap(), None).unwrap_err();
assert!(e.is_timeout(), "{}", e.message);
assert!(
started.elapsed() < Duration::from_secs(10),
"{:?}",
started.elapsed()
);
}
#[test]
fn a_deadline_reader_reads_nothing_before_it_is_read() {
struct Watched(Arc<AtomicBool>);
impl Read for Watched {
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
self.0.store(true, Ordering::Relaxed);
Ok(0)
}
}
let touched = Arc::new(AtomicBool::new(false));
let watched = Watched(touched.clone());
let mut reader =
DeadlineReader::new(watched, Instant::now(), Duration::from_secs(600)).unwrap();
std::thread::sleep(Duration::from_millis(100));
assert!(!touched.load(Ordering::Relaxed), "read before the run read");
let mut buf = [0u8; 8];
assert_eq!(reader.read(&mut buf).unwrap(), 0);
assert!(touched.load(Ordering::Relaxed));
}
#[test]
fn a_parse_of_few_steps_is_held_to_its_timeout_too() {
let src = format!("\"{}\"", "x".repeat(1 << 20));
match parse_within(&src, Format::Json, Some(Duration::from_millis(1))) {
Err(e) => assert!(e.is_timeout(), "{}", e.message),
Ok(_) => panic!("a parse over its time was let through"),
}
assert!(parse_within(&src, Format::Json, Some(Duration::from_secs(600))).is_ok());
}
#[test]
fn a_parse_that_ends_after_its_deadline_is_too_late() {
let silent = || Deadline {
at: Instant::now(),
limit: Duration::from_millis(1),
alarm: Some(Arc::new(AtomicBool::new(false))),
cancel: None,
};
for src in ["[1, 2, 3]", "[1, 2,"] {
let e = parse_here(src, Format::Json, Some(silent())).unwrap_err();
assert!(e.is_timeout(), "{src}: {}", e.message);
assert_eq!(e.message, "timeout: the parse ran longer than 0.001 s");
assert_eq!((e.line, e.col), (0, 0), "it stopped nowhere");
assert!(e.source_line.is_none());
assert!(
e.hint.starts_with("The parse finished, but it took "),
"{}",
e.hint
);
assert!(e.plain_report().starts_with("[aless/timeout]"));
}
let later = Deadline {
at: Instant::now() + Duration::from_secs(600),
..silent()
};
assert!(parse_here("[1, 2, 3]", Format::Json, Some(later)).is_ok());
}
#[test]
fn with_no_thread_to_keep_the_time_the_parse_reads_the_clock() {
let passed = Deadline {
at: Instant::now(),
limit: Duration::from_millis(1),
alarm: None,
cancel: None,
};
let e = parse_here(&many_tables(50), Format::Toml, Some(passed)).unwrap_err();
assert!(e.is_timeout(), "{}", e.message);
assert!(e.line > 0, "stopped where it had got to");
assert!(e.hint.contains("got this far"), "{}", e.hint);
}
fn parse_yaml_without_its_own_limit(src: &str) -> Result<Doc, LoadError> {
std::thread::scope(|scope| {
std::thread::Builder::new()
.stack_size(PARSE_STACK)
.spawn_scoped(scope, || {
let mut parser = make_parser(Format::Yaml)
.expect("YAML's grammar installs")
.expect("YAML has a grammar");
assert!(parser.parse_guards.contains_key("depth"));
parser.remove_parse_guard("depth");
parse_with(parser, src, Format::Yaml, None)
})
.expect("a parse thread")
.join()
.expect("the parse returns")
})
}
#[test]
fn nesting_deeper_than_the_cap_stops_cleanly() {
let yaml = "- ".repeat(50_000) + "x\n";
let e = parse_yaml_without_its_own_limit(&yaml).unwrap_err();
assert_eq!(e.code, "too_deep");
assert!(e.message.contains("about 1000 levels"), "{}", e.message);
assert!(e.plain_report().starts_with("[aless/too_deep]"));
assert!(parse_yaml_without_its_own_limit(&("- ".repeat(500) + "x\n")).is_ok());
let e = parse(&yaml, Format::Yaml).unwrap_err();
assert!(
e.message.contains("the yaml grammar reads"),
"{}",
e.message
);
}
#[test]
fn a_grammars_own_lower_limit_is_too_deep_too() {
let json = |n: usize| "[".repeat(n) + &"]".repeat(n);
let yaml = |n: usize| "- ".repeat(n) + "x\n";
let xml = |n: usize| "<a>".repeat(n) + &"</a>".repeat(n);
for (format, deep, within) in [
(Format::Json, json(200), json(100)),
(Format::Yaml, yaml(128), yaml(127)),
(Format::Xml, xml(257), xml(256)),
] {
let e = parse(&deep, format).unwrap_err();
assert_eq!(e.code, "too_deep", "{format}");
let own = format!("nested deeper than the {format} grammar reads");
assert!(e.message.contains(&own), "{}", e.message);
assert!(e.hint.contains("a nesting limit of its own"), "{}", e.hint);
assert!(e.plain_report().starts_with("[aless/too_deep]"));
assert!(parse(&within, format).is_ok(), "{format}");
}
let e = parse(&xml(257), Format::Xml).unwrap_err();
assert_eq!((e.line, e.col), (1, 256 * 3 + 1));
}
#[test]
fn running_out_of_input_says_so() {
for (src, format) in [
("{\"a\": 1, \"b\": ", Format::Json),
("[1, 2", Format::Json),
] {
let e = parse(src, format).unwrap_err();
assert_eq!(e.code, "unexpected");
assert_eq!(e.message, "unexpected end of input", "{src}");
assert!(e.hint.contains("ends before it is complete"), "{}", e.hint);
let report = e.plain_report();
assert!(report.contains("unexpected end of input"), "{report}");
assert!(!report.contains("character(s)"), "{report}");
}
let e = parse("[1, @]", Format::Json).unwrap_err();
assert_eq!(e.message, "unexpected character(s): @");
assert_eq!(e.source_line.as_deref(), Some("[1, @]"));
}
#[test]
fn parse_errors_carry_positions() {
let e = parse("{\"a\": 1,\n\"b\":\n", Format::Json).unwrap_err();
assert_eq!(e.line, 3);
assert!(e.message.starts_with("unexpected"), "{}", e.message);
assert_eq!(e.to_string(), format!("3:{}: {}", e.col, e.message));
let io = load_path(Path::new("/nonexistent/x.json"), None).unwrap_err();
assert_eq!(io.line, 0);
}
#[test]
fn reports_are_the_engines_with_the_file_named() {
let src = "{\n \"a\": 1,\n \"b\": [1, 2,,]\n}\n";
let e = parse(src, Format::Json)
.unwrap_err()
.with_origin("data.json");
assert_eq!((e.line, e.col), (3, 14));
let plain = e.plain_report();
let lines: Vec<&str> = plain.lines().collect();
assert_eq!(lines[0], "[tabnas/unexpected]: unexpected character(s): ,");
assert_eq!(lines[1], " --> data.json:3:14");
assert!(lines.contains(&" 3 | \"b\": [1, 2,,]"), "{plain}");
let caret = lines
.iter()
.position(|l| l.trim_start().starts_with('^'))
.unwrap();
assert_eq!(
lines[caret].find('^'),
lines[caret - 1].find(",,").map(|i| i + 1)
);
assert!(
plain.contains("do not match any rule alternative"),
"the hint: {plain}"
);
assert!(
e.report.contains("\x1b[91m"),
"the engine's colours are kept"
);
}
#[test]
fn control_characters_in_the_token_do_not_break_the_report() {
let e = parse("{\"a\": \"unterminated\n}\n", Format::Json).unwrap_err();
assert_eq!(e.message, "unprintable character: \\n");
let plain = e.plain_report();
let lines: Vec<&str> = plain.lines().collect();
assert_eq!(lines[0], "[tabnas/unprintable]: unprintable character: \\n");
assert!(lines[1].starts_with(" --> "), "{plain}");
assert!(
plain.contains("The character \\n (code point below 32) is not allowed inside a\n string literal."),
"{plain}"
);
assert_eq!(escape_controls("a\tb\u{1b}"), "a\\tb\\u001b");
}
#[test]
fn origins_are_relative_to_the_current_directory() {
let cwd = std::env::current_dir().unwrap();
assert_eq!(
origin_of(&cwd.join("a").join("b.json")),
Path::new("a").join("b.json").display().to_string()
);
assert_eq!(origin_of(Path::new("rel.json")), "rel.json");
let outside = std::env::temp_dir().join("x.json");
if !outside.starts_with(&cwd) {
assert_eq!(origin_of(&outside), outside.display().to_string());
}
}
#[test]
fn io_errors_report_in_the_same_layout() {
let e = load_path(Path::new("/nonexistent/x.json"), None).unwrap_err();
let plain = e.plain_report();
assert!(plain.starts_with("[aless/io]: "), "{plain}");
assert!(plain.contains(" --> /nonexistent/x.json"), "{plain}");
assert_eq!(strip_ansi("a\x1b[1;31mb\x1b]52;c;x\x07c\x1b[0m"), "abc");
}
#[test]
fn names_and_file_text_cannot_restyle_the_report() {
let e = LoadError::new("cannot read <no-file>").with_origin("evil\n\x1b[31mname");
let lines: Vec<&str> = e.report.lines().collect();
assert_eq!(lines.len(), 2, "{:?}", e.report);
assert!(lines[0].ends_with("cannot read <no-file>"), "{lines:?}");
assert!(lines[1].ends_with("evil\\n\\u001b[31mname"), "{lines:?}");
let e = parse("{\"a\": \"x\u{1b}[31my\"}", Format::Json).unwrap_err();
assert!(!e.report.contains("\u{1b}[31my"), "{:?}", e.report);
let plain = e.plain_report();
let lines: Vec<&str> = plain.lines().collect();
let quoted = lines.iter().position(|l| l.contains("x␛[31my")).unwrap();
let at = |l: &str, c: char| l.chars().position(|x| x == c);
assert_eq!(
at(lines[quoted + 1], '^'),
at(lines[quoted], '␛'),
"{plain}"
);
let e = parse("// \u{1b}[8mhidden\n{\"b\": [1,,2]}", Format::Jsonc).unwrap_err();
assert!(e.plain_report().contains(" 1 | // ␛[8mhidden"), "{e}");
}
#[test]
fn long_lines_are_cut_to_a_window_around_the_error() {
let body: Vec<String> = (0..1500).map(|i| i.to_string()).collect();
let src = format!("[{},,9]", body.join(","));
let col = src.find(",,").unwrap() + 2;
let e = parse(&src, Format::Json)
.unwrap_err()
.with_origin("min.json");
assert_eq!(e.col as usize, col);
let plain = e.plain_report();
let lines: Vec<&str> = plain.lines().collect();
assert_eq!(
lines[1],
format!(" --> min.json:1:{col}"),
"the file's column"
);
let quoted = lines
.iter()
.position(|l| l.starts_with(" 1 | …"))
.unwrap_or_else(|| panic!("{plain}"));
let caret = lines[quoted + 1].chars().position(|c| c == '^').unwrap();
assert!(caret < 60, "the caret is on screen: {plain}");
assert_eq!(lines[quoted].chars().nth(caret), Some(','), "{plain}");
let src = format!("[\"{}\",\n,1]", "x".repeat(300));
let e = parse(&src, Format::Json).unwrap_err();
assert_eq!((e.line, e.col), (2, 1));
let plain = e.plain_report();
let lines: Vec<&str> = plain.lines().collect();
let long = format!(" 1 | [\"{}…", "x".repeat(WINDOW - 2));
assert!(lines.contains(&long.as_str()), "{plain}");
let quoted = lines.iter().position(|l| l.starts_with(" 2 | ")).unwrap();
assert_eq!(
lines[quoted + 1].find('^'),
lines[quoted].find(','),
"{plain}"
);
}
#[test]
fn carets_stop_at_the_end_of_their_line() {
let e = parse("a: `abc\nb: 1\nc: 2\n", Format::Jsonic).unwrap_err();
let plain = e.plain_report();
let caret = plain
.lines()
.find(|l| l.trim_start().starts_with('^'))
.unwrap();
assert!(
caret
.trim_start()
.starts_with("^^^^ unterminated string: `abc\\nb"),
"{plain}"
);
}
#[test]
fn hints_keep_their_own_line_breaks() {
let e = parse("{a: 'abc\n, b: 1}\n", Format::Json5).unwrap_err();
let plain = e.plain_report();
assert!(
plain.contains("(character codes\n below 32). The character \\n is unprintable."),
"{plain}"
);
assert_eq!(
escape_hint(
"A \n B\nC q\u{1b}[1mr D",
Some("A {src} B\nC {name} D"),
"\n"
),
"A \\n B\nC q\\u001b[1mr D"
);
assert_eq!(
escape_hint("x \n y\u{7}\nz", Some("other"), "\n"),
"x \\n y\\u0007\nz"
);
assert_eq!(escape_hint("x \n y", None, "\n"), "x \\n y");
}
#[test]
fn grammar_codes_carry_their_own_hints() {
for (src, format, code) in [
("[s\nk = v\n", Format::Ini, "unterminated_section"),
("0X2A", Format::Zon, "zon_number"),
] {
let plain = parse(src, format).unwrap_err().plain_report();
assert!(plain.contains(&format!("/{code}]:")), "{plain}");
assert!(!plain.contains("probably a bug"), "{plain}");
}
}
#[test]
fn json5_continues_a_string_across_crlf() {
let lf = "{\n a: 'x\\\n y',\n b: [1, \"p\\\nq\"], // c \\\n c: 3\n}\n";
let crlf = lf.replace('\n', "\r\n");
let one = parse(lf, Format::Json5).unwrap();
let two = parse(&crlf, Format::Json5).unwrap_or_else(|e| panic!("{e}"));
let json = |d: &Doc| crate::fmt::to_json_compact(d, 0);
assert_eq!(json(&two), r#"{"a":"x y","b":[1,"pq"],"c":3}"#);
assert_eq!(json(&two), json(&one));
let at = |d: &Doc| d.nodes.iter().map(|n| (n.line, n.col)).collect::<Vec<_>>();
assert_eq!(at(&two), at(&one));
let lf = "['a\\\nb',\n @]";
let one = parse(lf, Format::Json5).unwrap_err();
let two = parse(&lf.replace('\n', "\r\n"), Format::Json5).unwrap_err();
assert_eq!((two.line, two.col), (one.line, one.col));
assert_eq!(one.line, 3);
assert!(parse("['a\\\\\r\nb']", Format::Json5).is_err());
let three = parse("['a\\\\\\\r\nb']", Format::Json5).unwrap();
assert_eq!(crate::fmt::to_json_compact(&three, 0), r#"["a\\b"]"#);
}
#[test]
fn csv_reads_control_characters_in_a_quoted_field() {
let src = "a,b,c\n\"x\ty\",\"\u{1e}\",\"\u{1f}z\"\n";
let doc = parse(src, Format::Csv).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(
crate::fmt::to_json_compact(&doc, 0),
r#"[{"a":"x\ty","b":"\u001e","c":"\u001fz"}]"#
);
}
#[test]
fn the_rule_history_bound_changes_no_value() {
let both = |format: Option<Format>, make: &dyn Fn() -> Tabnas, src: &str, what: &str| {
let bounded = make();
assert!(
bounded
.options
.rule
.history
.is_some_and(|h| h <= RULE_HISTORY),
"{what}"
);
let mut unbounded = make();
unbounded.options.rule.history = None;
let run = |p: Tabnas| {
p.parse(src)
.map_err(|e| e.to_string())
.and_then(|v| match format {
Some(format) => read_as(format, v).map_err(|e| e.to_string()),
None => Ok(v),
})
.map(|v| v.to_json())
};
assert_eq!(run(bounded), run(unbounded), "{what}");
};
for format in Format::ALL.iter().copied().filter(|f| *f != Format::Text) {
let make = || make_parser(format).unwrap().unwrap();
let items = |item: &dyn Fn(usize) -> String, sep: &str| {
(0..40).map(item).collect::<Vec<_>>().join(sep)
};
let long = match format {
Format::Json | Format::Jsonc | Format::Json5 | Format::Jsonic => format!(
"[{}]",
items(
&|i| format!("{{\"k{i}\": [{i}, \"v{i}\", {{\"n\": {i}}}]}}"),
", "
)
),
Format::Jsonl => items(&|i| format!("{{\"k\": {i}, \"v\": [1, 2]}}"), "\n"),
Format::Yaml => items(&|i| format!("- k: {i}\n v: [1, 2]\n w: x{i}"), "\n"),
Format::Toml => items(&|i| format!("[t{i}]\nk = {i}\nv = [1, 2]"), "\n"),
Format::Ini => items(&|i| format!("[s{i}]\nk = {i}\nv = x{i}"), "\n"),
Format::Csv => format!("a,b\n{}", items(&|i| format!("{i},x{i}"), "\n")),
Format::Tsv => format!("a\tb\n{}", items(&|i| format!("{i}\tx{i}"), "\n")),
Format::Xml => format!(
"<r>{}</r>",
items(&|i| format!("<i n=\"{i}\">x{i}</i>"), "")
),
Format::Zon => format!(".{{ {} }}", items(&|i| format!(".k{i} = {i}"), ", ")),
Format::Markdown => items(&|i| format!("- item {i}\n\npara {i}"), "\n\n"),
Format::Feed => format!(
"<rss version=\"2.0\"><channel><title>t</title>{}</channel></rss>",
items(&|i| format!("<item><title>i{i}</title></item>"), "")
),
Format::Css => items(&|i| format!(".c{i} {{ color: red; margin: {i}px }}"), "\n"),
Format::Proto => format!(
"syntax = \"proto3\";\n{}",
items(
&|i| format!("message M{i} {{ int32 a = 1; string b = 2; }}"),
"\n"
)
),
Format::Pgn => items(
&|i| format!("[Event \"e{i}\"]\n\n1. e4 e5 2. Nf3 Nc6 1-0"),
"\n\n",
),
Format::Expr => format!("[{}]", items(&|i| format!("{i}+{i}*({i}-1)"), ", ")),
Format::Semver => format!(
"1.2.3-{}+{}",
items(&|i| format!("a{i}"), "."),
items(&|i| format!("b{i}"), ".")
),
_ => continue,
};
both(
Some(format),
&make,
&long,
&format!("{format}: a long document"),
);
}
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let mut read = 0;
for entry in std::fs::read_dir(&fixtures).unwrap() {
let path = entry.unwrap().path();
let format = match fixture_format(&path) {
Some(Format::Text) | None => continue,
Some(format) => format,
};
let src = std::fs::read_to_string(&path).unwrap();
let make = || make_parser(format).unwrap().unwrap();
both(
Some(format),
&make,
parse_text(&src, format),
&path.display().to_string(),
);
read += 1;
}
let grammars = fixtures.join("grammars");
for entry in std::fs::read_dir(&grammars).unwrap() {
let path = entry.unwrap().path();
let sample = path.with_extension("sample");
if path.extension().and_then(|e| e.to_str()) != Some("abnf") || !sample.exists() {
continue;
}
let compiled = grammar::compile(&std::fs::read_to_string(&path).unwrap()).unwrap();
let make = || {
let mut parser = compiled.parser().unwrap();
parser.options.rule.history.get_or_insert(RULE_HISTORY);
parser
};
let src = std::fs::read_to_string(&sample).unwrap();
both(None, &make, &src, &path.display().to_string());
read += 1;
}
assert!(read >= 25, "{read} fixtures read");
let def = grammar::Definition::parse(
"--grammar-expr",
&format!("impl-kv-h={}", grammar::tests::KV),
)
.unwrap();
let custom = Format::Custom(grammar::register(def, Limits::NONE).unwrap());
let parser = make_parser(custom).unwrap().unwrap();
assert_eq!(parser.options.rule.history, Some(RULE_HISTORY));
}
#[test]
fn jsonc_accepts_a_trailing_comma() {
let json = |src: &str| {
let doc = parse(src, Format::Jsonc).unwrap_or_else(|e| panic!("{src:?}: {e}"));
crate::fmt::to_json_compact(&doc, 0)
};
assert_eq!(json("{\"a\": 1,}"), r#"{"a":1}"#);
assert_eq!(json("[1, [2,],]"), "[1,[2]]");
assert_eq!(
json("{\n // c\n \"a\": [1,],\n /* x */\n}"),
r#"{"a":[1]}"#
);
let doc = parse("{\n \"a\": 1,\n \"b\": 2,\n}", Format::Jsonc).unwrap();
let b = doc.resolve(&[Key::Name("b".into())]).unwrap() as usize;
assert_eq!((doc.nodes[b].line, doc.nodes[b].col), (3, 3));
assert!(parse("[1,,2]", Format::Jsonc).is_err());
}
fn fixture_format(path: &Path) -> Option<Format> {
Format::detect_known(path).or_else(|| {
path.extension()
.and_then(|e| e.to_str())
.and_then(Format::from_name)
.filter(|f| f.extensions().is_empty() && !f.is_custom())
})
}
#[test]
fn every_format_parses_its_sample() {
let samples: [(Format, &str); 19] = [
(Format::Json, "{\"a\": [1, 2]}"),
(Format::Jsonl, "{\"a\": 1}\n{\"a\": 2}\n"),
(Format::Jsonic, "a: 1, b: {c: x}"),
(Format::Jsonc, "// c\n{\"a\": 1, /* x */ \"b\": 2}"),
(Format::Json5, "{a: 'x', b: 0x10,}"),
(Format::Yaml, "a: 1\nb:\n - x\n - y\n"),
(Format::Toml, "title = \"t\"\n[owner]\nname = \"ada\"\n"),
(Format::Ini, "[s]\nk = v\n"),
(Format::Csv, "a,b\n1,2\n"),
(Format::Tsv, "a\tb\n1\t2\n"),
(Format::Xml, "<r><i id=\"1\">x</i></r>"),
(Format::Zon, ".{ .name = \"z\", .ok = true }"),
(Format::Markdown, "# T\n\npara\n"),
(
Format::Feed,
"<?xml version=\"1.0\"?><rss version=\"2.0\"><channel><title>t</title><item><title>i</title></item></channel></rss>",
),
(Format::Css, "a { color: red }\n@media print { b { margin: 0 } }\n"),
(
Format::Proto,
"syntax = \"proto3\";\nmessage M { int32 a = 1; }\n",
),
(Format::Pgn, "[Event \"e\"]\n\n1. e4 e5 1-0\n"),
(Format::Expr, "a: 1+2*3, b: (4)\n"),
(Format::Semver, "1.2.3-rc.1+build.5\n"),
];
assert_eq!(samples.len() + 1, Format::ALL.len());
assert!(Format::ALL.contains(&Format::Text));
assert!(!Format::ALL.iter().any(|f| f.is_custom()));
for (format, src) in samples {
let doc = parse(src, format).unwrap_or_else(|e| panic!("{format}: {e}"));
assert!(doc.len() > 1, "{format} produced a scalar-only document");
assert!(
doc.nodes.iter().any(|n| n.line > 0),
"{format}: no node carries a source line"
);
}
}
fn read_json(src: &str, format: Format) -> String {
let doc = parse(src, format).unwrap_or_else(|e| panic!("{format} {src:?}: {e}"));
crate::fmt::to_json_compact(&doc, 0)
}
#[test]
fn proto_reads_as_its_descriptor() {
assert_eq!(
read_json(
"syntax = \"proto3\";\npackage p;\nmessage M { int32 a = 1; string b = 1_0; }",
Format::Proto
),
r#"{"package":"p","dependency":[],"publicDependency":[],"weakDependency":[],"messageType":[{"name":"M","field":[{"name":"a","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_INT32"},{"name":"b","number":null,"label":"LABEL_OPTIONAL","type":"TYPE_STRING"}],"nestedType":[],"enumType":[],"oneofDecl":[],"extension":[]}],"enumType":[],"service":[],"extension":[],"syntax":"proto3"}"#
);
assert!(read_json("message M {}", Format::Proto).ends_with(r#""syntax":"proto2"}"#));
}
#[test]
fn proto_refuses_what_its_crate_refuses() {
let cap = tabnas_proto::MAX_NESTING_DEPTH;
let nested = |n: usize| format!("{}{}", "message M {".repeat(n), "}".repeat(n));
assert!(parse(&nested(cap), Format::Proto).is_ok());
let e = parse(&nested(cap + 1), Format::Proto).unwrap_err();
assert_eq!(e.code, "too_deep");
assert_eq!(
e.message,
"too_deep: nested deeper than the proto grammar reads"
);
assert!(e.hint.contains("more than 100 levels deep"), "{}", e.hint);
assert_eq!((e.line, e.col), (0, 0));
let e = parse("syntax = \"proto5\";\nmessage M {}\n", Format::Proto).unwrap_err();
assert_eq!(e.code, "version");
assert_eq!(e.message, "version: unknown syntax version \"proto5\"");
assert!(
e.plain_report()
.starts_with("[proto/version]: version: unknown syntax version \"proto5\""),
"{}",
e.plain_report()
);
assert!(!e.is_io() && !e.is_too_large() && !e.is_timeout());
}
#[test]
fn expr_reads_as_its_s_expressions() {
assert_eq!(read_json("1+2*3", Format::Expr), r#"["+",1,["*",2,3]]"#);
assert_eq!(
read_json("a: (1+2)*3, b: [-4, x]", Format::Expr),
r#"{"a":["*",["(",["+",1,2]],3],"b":[["-",4],"x"]}"#
);
let sum = |ops: usize| vec!["1"; ops + 1].join("+");
let limit = tabnas_expr::NODE_LIMIT;
assert!(parse(&sum(limit), Format::Expr).is_ok());
let e = parse(&sum(limit + 1), Format::Expr).unwrap_err();
assert_eq!(e.code, "too_deep");
assert_eq!(
e.message,
"too_deep: an expression larger or deeper than the expr grammar reads"
);
assert!(e.hint.contains("127 operations"), "{}", e.hint);
}
#[test]
fn a_version_reads_without_its_line_break() {
let version = r#"{"major":1,"minor":2,"patch":3,"prerelease":["rc",1],"build":[]}"#;
for src in [
"1.2.3-rc.1",
"1.2.3-rc.1\n",
"1.2.3-rc.1\r\n",
"\u{feff}1.2.3-rc.1\n",
] {
assert_eq!(read_json(src, Format::Semver), version, "{src:?}");
}
for src in ["1.2.3\n\n", " 1.2.3", "v1.2.3\n", "1.2\n"] {
assert!(parse(src, Format::Semver).is_err(), "{src:?}");
}
}
#[test]
fn css_and_pgn_read_as_their_grammars_build_them() {
assert_eq!(
read_json("a { color: red }", Format::Css),
r#"{"type":"stylesheet","rules":[{"type":"rule","selectors":["a"],"declarations":[{"type":"declaration","property":"color","value":"red"}]}]}"#
);
let games = read_json("[White \"a\"]\n\n1. e4 1-0\n\n1. d4 *\n", Format::Pgn);
assert!(
games.starts_with(r#"[{"tags":{"White":"a"},"moves":[{"san":"e4""#),
"{games}"
);
assert!(games.ends_with(r#""result":"*"}]"#), "{games}");
assert!(
make_parser(Format::Pgn)
.unwrap()
.unwrap()
.options
.color
.active
);
let e = parse("1. e4 Zz9", Format::Pgn).unwrap_err();
assert_eq!(e.code, "unexpected");
assert!(e.message.contains("not chess notation"), "{}", e.message);
}
#[test]
fn positions_reach_the_tree() {
let doc = parse("[s]\nk = v\n", Format::Ini).unwrap();
let k = doc
.resolve(&[Key::Name("s".into()), Key::Name("k".into())])
.unwrap();
assert_eq!(doc.node(k).line, 2);
let doc = parse("a,b\n1,2\n3,4\n", Format::Csv).unwrap();
let second = doc.resolve(&[Key::Index(1)]).unwrap();
assert_eq!(doc.node(second).line, 3);
let first = doc.resolve(&[Key::Index(0)]).unwrap();
assert_eq!(doc.node(first).line, 2);
let a = doc
.resolve(&[Key::Index(0), Key::Name("a".into())])
.unwrap();
assert_eq!((doc.node(a).line, doc.node(a).col), (2, 1));
}
}