pub(crate) fn classify(msg: &str) -> Option<String> {
let code: Vec<&str> = if msg.starts_with("wrong # args:") {
vec!["TCL", "WRONGARGS"]
} else if msg == "divide by zero" {
vec!["ARITH", "DIVZERO", msg]
} else if msg == "domain error: argument not in valid range" {
vec!["ARITH", "DOMAIN", msg]
} else if let Some(desc) = operand_description(msg) {
vec!["ARITH", "DOMAIN", desc]
} else if let Some(name) = quoted(msg, "invalid command name \"", "\"") {
vec!["TCL", "LOOKUP", "COMMAND", name]
} else if let Some(name) = quoted(msg, "can't rename \"", "\": command doesn't exist")
.or_else(|| quoted(msg, "can't delete \"", "\": command doesn't exist"))
{
vec!["TCL", "LOOKUP", "COMMAND", name]
} else if let Some(key) = quoted(msg, "key \"", "\" not known in dictionary") {
vec!["TCL", "LOOKUP", "DICT", key]
} else if let Some(name) = quoted(msg, "can not find channel named \"", "\"") {
vec!["TCL", "LOOKUP", "CHANNEL", name]
} else if let Some(name) = quoted(msg, "unknown encoding \"", "\"") {
vec!["TCL", "LOOKUP", "ENCODING", name]
} else if let Some(sub) = msg
.strip_prefix("unknown or ambiguous subcommand \"")
.and_then(|rest| rest.split_once("\": must be "))
.map(|(sub, _)| sub)
{
vec!["TCL", "LOOKUP", "SUBCOMMAND", sub]
} else if msg.starts_with("bad index \"")
&& msg.ends_with("\": must be integer?[+-]integer? or end?[+-]integer?")
{
vec!["TCL", "VALUE", "INDEX"]
} else if msg.starts_with("expected number but got ") {
vec!["TCL", "VALUE", "NUMBER"]
} else if quoted(msg, "illegal access mode \"", "\"").is_some() {
vec!["TCL", "OPENMODE", "INVALID"]
} else if let Some((name, text)) = posix(msg) {
return Some(crate::list::join(&["POSIX", name, text.as_str()]));
} else {
return None;
};
Some(crate::list::join(&code))
}
fn quoted<'a>(msg: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
msg.strip_prefix(prefix)?.strip_suffix(suffix)
}
fn operand_description(msg: &str) -> Option<&'static str> {
let rest = msg.strip_prefix("cannot use ")?;
if rest.starts_with("a list as ") && rest.contains("operand of \"") {
return Some("list");
}
[
"non-numeric string",
"non-numeric floating-point value",
"floating-point value",
"(big) integer",
]
.into_iter()
.find(|d| {
rest.strip_prefix(d)
.is_some_and(|r| r.starts_with(" \"") && r.contains("operand of \""))
})
}
fn posix(msg: &str) -> Option<(&'static str, String)> {
let (_, reason) = msg.rsplit_once(": ")?;
ERRNO_NAMES.iter().find_map(|&(code, name)| {
let text = strerror(code)?;
(text == reason).then_some((name, text))
})
}
fn strerror(code: i32) -> Option<String> {
let text = unsafe { libc::strerror(code) };
if text.is_null() {
return None;
}
Some(unsafe { std::ffi::CStr::from_ptr(text) }.to_string_lossy().to_lowercase())
}
const ERRNO_NAMES: &[(i32, &str)] = &[
(libc::EPERM, "EPERM"),
(libc::ENOENT, "ENOENT"),
(libc::ESRCH, "ESRCH"),
(libc::EINTR, "EINTR"),
(libc::EIO, "EIO"),
(libc::ENXIO, "ENXIO"),
(libc::E2BIG, "E2BIG"),
(libc::ENOEXEC, "ENOEXEC"),
(libc::EBADF, "EBADF"),
(libc::ECHILD, "ECHILD"),
(libc::EAGAIN, "EAGAIN"),
(libc::ENOMEM, "ENOMEM"),
(libc::EACCES, "EACCES"),
(libc::EFAULT, "EFAULT"),
(libc::EBUSY, "EBUSY"),
(libc::EEXIST, "EEXIST"),
(libc::EXDEV, "EXDEV"),
(libc::ENODEV, "ENODEV"),
(libc::ENOTDIR, "ENOTDIR"),
(libc::EISDIR, "EISDIR"),
(libc::EINVAL, "EINVAL"),
(libc::ENFILE, "ENFILE"),
(libc::EMFILE, "EMFILE"),
(libc::ENOTTY, "ENOTTY"),
(libc::ETXTBSY, "ETXTBSY"),
(libc::EFBIG, "EFBIG"),
(libc::ENOSPC, "ENOSPC"),
(libc::ESPIPE, "ESPIPE"),
(libc::EROFS, "EROFS"),
(libc::EMLINK, "EMLINK"),
(libc::EPIPE, "EPIPE"),
(libc::EDOM, "EDOM"),
(libc::ERANGE, "ERANGE"),
(libc::EDEADLK, "EDEADLK"),
(libc::ENAMETOOLONG, "ENAMETOOLONG"),
(libc::ENOLCK, "ENOLCK"),
(libc::ENOSYS, "ENOSYS"),
(libc::ENOTEMPTY, "ENOTEMPTY"),
(libc::ELOOP, "ELOOP"),
(libc::ENOTSOCK, "ENOTSOCK"),
(libc::EDESTADDRREQ, "EDESTADDRREQ"),
(libc::EMSGSIZE, "EMSGSIZE"),
(libc::EPROTOTYPE, "EPROTOTYPE"),
(libc::ENOPROTOOPT, "ENOPROTOOPT"),
(libc::EPROTONOSUPPORT, "EPROTONOSUPPORT"),
(libc::EAFNOSUPPORT, "EAFNOSUPPORT"),
(libc::EADDRINUSE, "EADDRINUSE"),
(libc::EADDRNOTAVAIL, "EADDRNOTAVAIL"),
(libc::ENETDOWN, "ENETDOWN"),
(libc::ENETUNREACH, "ENETUNREACH"),
(libc::ENETRESET, "ENETRESET"),
(libc::ECONNABORTED, "ECONNABORTED"),
(libc::ECONNRESET, "ECONNRESET"),
(libc::ENOBUFS, "ENOBUFS"),
(libc::EISCONN, "EISCONN"),
(libc::ENOTCONN, "ENOTCONN"),
(libc::ETIMEDOUT, "ETIMEDOUT"),
(libc::ECONNREFUSED, "ECONNREFUSED"),
(libc::EHOSTDOWN, "EHOSTDOWN"),
(libc::EHOSTUNREACH, "EHOSTUNREACH"),
(libc::EALREADY, "EALREADY"),
(libc::EINPROGRESS, "EINPROGRESS"),
];
#[cfg(test)]
mod tests {
use super::classify;
#[test]
fn an_ambiguous_template_stays_unclassified() {
assert_eq!(classify("expected integer but got \"x\""), None);
assert_eq!(classify("can't read \"x\": no such variable"), None);
assert_eq!(classify("bad option \"-x\": must be -a or -b"), None);
}
#[test]
fn a_code_element_with_a_space_keeps_its_structure() {
assert_eq!(
classify("invalid command name \"a b\"").as_deref(),
Some("TCL LOOKUP COMMAND {a b}")
);
assert_eq!(
classify("couldn't open \"/x\": no such file or directory").as_deref(),
Some("POSIX ENOENT {no such file or directory}")
);
}
}