pub const FORMULA_PREFIXES: [char; 6] = ['=', '+', '-', '@', '\t', '\r'];
#[must_use]
pub fn escape(field: &str) -> String {
let guarded = if field.starts_with(FORMULA_PREFIXES) {
format!("'{field}")
} else {
field.to_owned()
};
if guarded.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", guarded.replace('"', "\"\""))
} else {
guarded
}
}
#[must_use]
pub fn row(fields: &[&str]) -> String {
let mut line = fields
.iter()
.map(|field| escape(field))
.collect::<Vec<_>>()
.join(",");
line.push('\n');
line
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_fields_pass_through() {
assert_eq!(escape("nick@example.com"), "nick@example.com");
assert_eq!(escape("pending"), "pending");
}
#[test]
fn formula_leading_cells_are_prefixed() {
for evil in ["=cmd|' /C", "+1", "-1", "@SUM(A1)", "\ttab", "\rCR"] {
let escaped = escape(evil);
let unquoted = escaped.trim_matches('"');
assert!(unquoted.starts_with('\''), "{evil:?} -> {escaped:?}");
assert!(!unquoted.starts_with(evil.chars().next().unwrap()));
}
}
#[test]
fn quoting_doubles_embedded_quotes() {
assert_eq!(escape("a\"b"), "\"a\"\"b\"");
assert_eq!(escape("a,b"), "\"a,b\"");
}
#[test]
fn formula_guard_applies_before_quoting() {
assert_eq!(escape("=a,b"), "\"'=a,b\"");
}
#[test]
fn row_joins_and_terminates() {
assert_eq!(row(&["a", "=b", "c"]), "a,'=b,c\n");
}
}