mod apply;
mod calc;
mod census;
mod certify;
mod diff;
mod hash;
mod inspect;
mod journal;
mod log;
mod ooxml;
mod patch;
mod refshift;
mod restructure;
mod structural;
mod undo;
mod value;
mod verify;
#[cfg(test)]
pub(crate) mod testkit;
#[cfg(test)]
mod tests_algebra;
#[cfg(test)]
mod tests_cache_soundness;
#[cfg(test)]
mod tests_corpus_lint;
#[cfg(test)]
mod tests_value_faithful;
use clap::{Parser, Subcommand};
use std::sync::Mutex;
#[derive(Parser)]
#[command(name = "xlq", version, about, propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Inspect {
file: String,
#[arg(long)]
redact: bool,
},
Diff {
old: String,
new: String,
},
Calc {
file: String,
},
Apply {
file: Option<String>,
patch: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
actor: Option<String>,
#[arg(long)]
schema: bool,
},
Restructure {
file: String,
#[arg(long)]
sheet: String,
#[arg(long)]
op: String,
#[arg(long)]
at: u32,
#[arg(long, default_value_t = 1)]
count: u32,
#[arg(long, default_value_t = 0)]
dest: u32,
#[arg(long)]
dry_run: bool,
#[arg(long)]
actor: Option<String>,
},
Certify {
original: String,
edited: String,
#[arg(long)]
sheet: String,
#[arg(long)]
op: String,
#[arg(long)]
at: u32,
#[arg(long, default_value_t = 1)]
count: u32,
#[arg(long, default_value_t = 0)]
dest: u32,
},
Log {
file: String,
},
Verify {
file: String,
},
Undo {
file: String,
#[arg(long)]
actor: Option<String>,
},
#[command(name = "__panic", hide = true)]
Panic,
#[command(name = "__shift-formula-batch", hide = true)]
ShiftFormulaBatch,
}
fn parse_structural_op(op: &str) -> Option<(refshift::Axis, refshift::Op)> {
use refshift::{Axis, Op};
match op {
"insert-rows" => Some((Axis::Row, Op::Insert)),
"delete-rows" => Some((Axis::Row, Op::Delete)),
"insert-cols" => Some((Axis::Col, Op::Insert)),
"delete-cols" => Some((Axis::Col, Op::Delete)),
"move-rows" => Some((Axis::Row, Op::Move)),
_ => None,
}
}
fn batch_die(lineno: usize, msg: &str) -> ! {
eprintln!("__shift-formula-batch: line {lineno}: {msg}");
std::process::exit(2);
}
fn shift_formula_batch() {
use refshift::{Axis, Op, StructuralEdit};
use std::io::BufRead;
let stdin = std::io::stdin();
for (idx, line) in stdin.lock().lines().enumerate() {
let lineno = idx + 1;
let line = line.unwrap_or_else(|e| batch_die(lineno, &format!("stdin read: {e}")));
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() != 5 {
batch_die(
lineno,
"expected 5 tab-separated fields: formula, axis, op, at, count",
);
}
let formula = fields[0];
let axis = match fields[1] {
"row" => Axis::Row,
"col" => Axis::Col,
_ => batch_die(lineno, "axis must be row|col"),
};
let op = match fields[2] {
"insert" => Op::Insert,
"delete" => Op::Delete,
_ => batch_die(lineno, "op must be insert|delete"),
};
let at: u32 = fields[3]
.parse()
.unwrap_or_else(|_| batch_die(lineno, "at must be a u32"));
let count: u32 = fields[4]
.parse()
.unwrap_or_else(|_| batch_die(lineno, "count must be a u32"));
if refshift::has_unquoted_non_ascii_qualifier(formula) {
println!("__REFUSE__");
continue;
}
let edit = StructuralEdit {
axis,
at,
count,
op,
sheet: "S".into(),
dest: 0,
};
let (shifted, _) = refshift::shift_formula(formula, "S", &edit);
println!("{}", shifted.replace('\t', "\\t").replace('\n', "\\n"));
}
}
static PANIC_MSG: Mutex<Option<String>> = Mutex::new(None);
fn install_panic_hook() {
std::panic::set_hook(Box::new(|info| {
let payload = info
.payload()
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "internal panic".to_string());
let loc = info
.location()
.map(|l| {
let base = std::path::Path::new(l.file())
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
format!("{base}:{}", l.line())
})
.unwrap_or_else(|| "unknown".to_string());
if let Ok(mut slot) = PANIC_MSG.lock() {
*slot = Some(format!("internal error at {loc}: {payload}"));
}
}));
}
fn main() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
install_panic_hook();
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)).is_err() {
let msg = PANIC_MSG
.lock()
.ok()
.and_then(|m| m.clone())
.unwrap_or_else(|| "internal error".to_string());
eprintln!("xlq internal error: {msg}");
println!(
"{}",
serde_json::json!({ "error": msg, "internal_error": true })
);
std::process::exit(70);
}
}
fn run() {
let cli = Cli::parse();
let result = match cli.command {
Command::Inspect { file, redact } => inspect::run(&file, redact),
Command::Diff { old, new } => diff::run(&old, &new),
Command::Calc { file } => calc::run(&file),
Command::Apply {
file,
patch,
dry_run,
actor,
schema,
} => {
if schema {
Ok(serde_json::json!({ "command": "apply", "schema": patch::schema() }))
} else {
match (file, patch) {
(Some(f), Some(p)) => apply::run(&f, &p, dry_run, actor.as_deref()),
_ => Ok(serde_json::json!({
"command": "apply",
"error": "bad_args",
"reason": "file and patch are required unless --schema is given",
})),
}
}
}
Command::Restructure {
file,
sheet,
op,
at,
count,
dest,
dry_run,
actor,
} => match parse_structural_op(&op) {
Some((axis, operation)) => restructure::run(
&file,
&sheet,
axis,
operation,
at,
count,
dest,
dry_run,
actor.as_deref(),
),
None => Ok(serde_json::json!({
"command": "restructure",
"error": "bad_op",
"reason": "--op must be insert-rows | delete-rows | insert-cols | delete-cols | move-rows",
})),
},
Command::Certify {
original,
edited,
sheet,
op,
at,
count,
dest,
} => certify::run(&original, &edited, &sheet, &op, at, count, dest),
Command::Log { file } => log::run(&file),
Command::Verify { file } => verify::run(&file),
Command::Undo { file, actor } => undo::run(&file, actor.as_deref()),
Command::Panic => panic!("deliberate test panic — firewall check"),
Command::ShiftFormulaBatch => {
shift_formula_batch();
return;
}
};
match result {
Ok(value) => {
println!(
"{}",
serde_json::to_string_pretty(&value).expect("serialize report")
);
let code = outcome_exit_code(&value);
if code != 0 {
std::process::exit(code);
}
}
Err(err) => {
eprintln!("xlq error: {err:#}");
let payload = serde_json::json!({ "error": format!("{err:#}") });
println!("{payload}");
std::process::exit(1);
}
}
}
fn outcome_exit_code(v: &serde_json::Value) -> i32 {
if let Some(kind) = v.get("error").and_then(|e| e.as_str()) {
return if matches!(kind, "bad_op" | "bad_args") {
2
} else {
1
};
}
if v.get("error").is_some() {
return 1;
}
if v.get("status").and_then(|s| s.as_str()) == Some("REFUSED") {
return 1;
}
if v.get("verified") == Some(&serde_json::Value::Bool(false)) {
return 1;
}
0
}