use crate::intrinsics::fold;
use crate::refers::{bracket_group_end, is_ident_char, skip_delimited};
pub const EGRESS_NAMES: &[&str] = &[
"webservice",
"filterxml",
"hyperlink",
"rtd",
"image",
"stockhistory",
"call",
"register",
"register.id",
"exec",
"initiate",
"execute",
"poke",
"request",
"send.mail",
"dde",
"importxml",
"importdata",
"importhtml",
"importrange",
"importfeed",
"googlefinance",
"googletranslate",
];
pub const DDE: &str = "DDE";
pub fn egress_call(formula_text: &str) -> Option<String> {
let s: Vec<char> = formula_text.chars().collect();
let n = s.len();
let mut i = 0;
while i < n {
let ch = s[i];
if ch == '"' {
i = skip_delimited(&s, i, '"');
} else if ch == '\'' {
i = skip_delimited(&s, i, '\'');
} else if ch == '[' {
i = bracket_group_end(&s, i);
} else if ch == '|' {
return Some(DDE.to_string());
} else if is_ident_char(ch) {
let mut k = i;
while k < n && is_ident_char(s[k]) {
k += 1;
}
let mut tok = fold(&s[i..k].iter().collect::<String>());
i = k;
while i < n && matches!(s[i], ' ' | '\t' | '\r' | '\n') {
i += 1;
}
if i < n && s[i] == '(' {
loop {
let stripped = tok
.strip_prefix("_xlfn.")
.or_else(|| tok.strip_prefix("_xlws."))
.map(str::to_string);
match stripped {
Some(t) => tok = t,
None => break,
}
}
if EGRESS_NAMES.contains(&tok.as_str()) {
return Some(tok.to_ascii_uppercase());
}
}
} else {
i += 1;
}
}
None
}
pub fn report_for(body: &[&str]) -> String {
let mut text = body.join("\n");
while text.ends_with('\n') {
text.pop();
}
match egress_call(&text) {
Some(name) => format!("REFUSED\t{name}\r\n"),
None => "WRITTEN\r\n".to_string(),
}
}
pub fn fixture_report(fixture: &str) -> String {
let text = fixture.replace("\r\n", "\n");
let mut out = String::new();
let mut header: Option<&str> = None;
let mut body: Vec<&str> = Vec::new();
for line in text.split('\n') {
if line.starts_with("=== ") {
if let Some(h) = header {
out.push_str(h);
out.push_str("\r\n");
out.push_str(&report_for(&body));
}
header = Some(line);
body.clear();
} else if header.is_some() {
body.push(line);
}
}
if let Some(h) = header {
out.push_str(h);
out.push_str("\r\n");
out.push_str(&report_for(&body));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const FIXTURE: &str = include_str!("../../scripts/egress.txt");
const GOLDEN: &str = include_str!("../../scripts/egress_golden.txt");
#[test]
fn the_egress_golden_is_reproduced() {
let norm = |s: &str| s.replace("\r\n", "\n").trim_end_matches('\n').to_string();
let got = norm(&fixture_report(FIXTURE));
let want = norm(GOLDEN);
if got != want {
let at = got
.chars()
.zip(want.chars())
.position(|(a, b)| a != b)
.unwrap_or(got.len().min(want.len()));
let line = want[..at].matches('\n').count() + 1;
let context = |s: &str| {
let start = s[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
let end = s[at..].find('\n').map(|i| at + i).unwrap_or(s.len());
s[start..end].to_string()
};
panic!(
"the egress golden differs at char {at} (line {line}):\n want: {}\n got: {}",
context(&want),
context(&got)
);
}
}
#[test]
fn the_pieces_the_table_reaches_only_through_the_whole() {
assert_eq!(egress_call(""), None);
assert_eq!(egress_call("=A1+B1"), None);
assert_eq!(
egress_call("=register.id(1)").as_deref(),
Some("REGISTER.ID")
);
assert_eq!(egress_call("=a|b!c").as_deref(), Some("DDE"));
for name in EGRESS_NAMES {
let want = name.to_ascii_uppercase();
assert_eq!(egress_call(&format!("={name}(1)")), Some(want), "{name}");
assert_eq!(egress_call(&format!("={name}+1")), None, "{name}");
assert_eq!(egress_call(&format!("=\"{name}(\"")), None, "{name}");
}
assert_eq!(EGRESS_NAMES.len(), 23);
}
#[test]
#[ignore]
fn print_egress_report() {
print!("{}", fixture_report(FIXTURE));
}
}