use std::path::PathBuf;
use std::process::ExitCode;
use std::time::{SystemTime, UNIX_EPOCH};
mod bundled_scenarios;
mod live_io;
const USAGE: &str = "usage: kshana <scenario.toml> [--study-name <s>] [--eop <finals2000A>] [--export-sp3 <out.sp3>] [--export-omm <out.omm>] [--export-oem <out.oem>] [--export <czml|kml|geojson|stk|sigmf|all|list>]... [--import-route <route.geojson>]
or: kshana <scenario.toml> --animate <svg|html|frames|all> [--animate-fps <n>] [--animate-duration <s>]
or: kshana --study <suite.toml>
or: kshana --validate <scenario.toml>
or: kshana receiver-trust <scenario.toml>
or: kshana receiver-trust live <session.toml> [--stdin | --file <path> [--follow] | --tcp <host:port> | --udp <port>] [--gate] (kshana receiver-trust live --help)
or: kshana receiver-trust evidence <scenario.toml> --from <t0> --to <t1> --key <keyfile> --out <dir> (kshana evidence --help)
or: kshana evidence <keygen|verify|attach-timestamp> ... (kshana evidence --help)
or: kshana trust-telemetry [options] (kshana trust-telemetry --help)
or: kshana nmea-scenario <scenario.toml> [--out <file.nmea>] [--tcp <addr>|--udp <addr>] (kshana nmea-scenario --help)
or: kshana interference-map <adsb|ais|fetch-land> ... (kshana interference-map --help)
or: kshana route-exposure --route <route> --map <map.geojson|dir> [--from <date>] [--to <date>]
or: kshana bench-export <scenario.toml> [--out <base>] [--epoch <YYYY-MM-DDTHH:MM:SS>]
or: kshana compliance-report [--out <base>] <result.json>... | --mapping | --sources
or: kshana osnma verify <pages|vectors.csv|stream.ubx> [--merkle-root <hex>] [--public-key <id:type:hex>] [--reference-time <gst-s>] [--json] (kshana osnma --help)
or: kshana iq <scene|acquire|track|sweep|labfit|campaign|conditions|frontend|monitor|inventory|info|extract|convert|decimate> ... (kshana iq --help)
or: kshana kinds [--json]
or: kshana example [<name>]
or: kshana --help | --version";
fn utc_iso8601_now() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = (secs / 86_400) as i64;
let tod = secs % 86_400;
let (hour, min, sec) = (tod / 3600, (tod % 3600) / 60, tod % 60);
let (year, month, day) = civil_from_days(days);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (if m <= 2 { y + 1 } else { y }, m, d)
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("kinds") {
return print_kinds(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("example") {
return print_example(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("evidence") {
return ExitCode::from(kshana::evidence::cli::run_evidence(&args[2..]) as u8);
}
if args.get(1).map(String::as_str) == Some("receiver-trust")
&& args.get(2).map(String::as_str) == Some("evidence")
{
return ExitCode::from(
kshana::evidence::cli::run_receiver_trust_evidence(&args[3..]) as u8,
);
}
if args.get(1).map(String::as_str) == Some("trust-telemetry") {
return ExitCode::from(kshana::telemetry::cli::run(&args[2..]) as u8);
}
if args.get(1).map(String::as_str) == Some("receiver-trust") {
return run_receiver_trust_cli(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("nmea-scenario") {
return kshana::nmea_synth::cli::run_cli(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("interference-map") {
return ExitCode::from(kshana::interference_map::cli::run_map(&args[2..]) as u8);
}
if args.get(1).map(String::as_str) == Some("route-exposure") {
return ExitCode::from(kshana::interference_map::cli::run_route(&args[2..]) as u8);
}
if args.get(1).map(String::as_str) == Some("bench-export") {
return run_bench_export_cli(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("compliance-report") {
return run_compliance_report_cli(&args[2..]);
}
if args.get(1).map(String::as_str) == Some("iq") {
return ExitCode::from(kshana::iq::cli::run(&args[2..]) as u8);
}
if args.get(1).map(String::as_str) == Some("osnma") {
return ExitCode::from(kshana::osnma::cli::run(&args[2..]) as u8);
}
let mut positional: Option<String> = None;
let mut export_sp3_path: Option<PathBuf> = None;
let mut export_omm_path: Option<PathBuf> = None;
let mut export_oem_path: Option<PathBuf> = None;
let mut eop_path: Option<PathBuf> = None;
let mut exports: Vec<String> = Vec::new();
let mut route_path: Option<PathBuf> = None;
let mut study_name: Option<String> = None;
let mut study_suite_path: Option<PathBuf> = None;
let mut validate_path: Option<PathBuf> = None;
let mut animate_formats: Vec<kshana::animation::AnimationFormat> = Vec::new();
let mut animate_opts = kshana::animation::AnimationOptions::default();
let mut animate_tuned = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--help" | "-h" | "help" => {
println!("{USAGE}");
return ExitCode::SUCCESS;
}
"--version" | "-V" => {
println!("kshana {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
"--validate" => {
i += 1;
match args.get(i) {
Some(p) => validate_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --validate needs a scenario path");
return ExitCode::from(2);
}
}
}
"--study" => {
i += 1;
match args.get(i) {
Some(p) => study_suite_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --study needs a suite manifest path");
return ExitCode::from(2);
}
}
}
"--study-name" => {
i += 1;
match args.get(i) {
Some(s) => study_name = Some(s.to_string()),
None => {
eprintln!("error: --study-name needs a value");
return ExitCode::from(2);
}
}
}
"--export-sp3" => {
i += 1;
match args.get(i) {
Some(p) => export_sp3_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --export-sp3 needs a path");
return ExitCode::from(2);
}
}
}
"--export-omm" => {
i += 1;
match args.get(i) {
Some(p) => export_omm_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --export-omm needs a path");
return ExitCode::from(2);
}
}
}
"--export-oem" => {
i += 1;
match args.get(i) {
Some(p) => export_oem_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --export-oem needs a path");
return ExitCode::from(2);
}
}
}
"--animate" => {
i += 1;
let Some(v) = args.get(i) else {
eprintln!("error: --animate needs a format: svg, html, frames or all");
return ExitCode::from(2);
};
match kshana::animation::AnimationFormat::parse_list(v) {
Ok(list) => {
for f in list {
if !animate_formats.contains(&f) {
animate_formats.push(f);
}
}
}
Err(e) => {
eprintln!("error: --animate: {e}");
return ExitCode::from(2);
}
}
}
"--animate-fps" => {
i += 1;
match args.get(i).and_then(|v| v.parse::<u32>().ok()) {
Some(n) => {
animate_opts.fps = n;
animate_tuned = true;
}
None => {
eprintln!("error: --animate-fps needs a whole number of frames per second");
return ExitCode::from(2);
}
}
}
"--export" => {
i += 1;
match args.get(i) {
Some(f) => exports.push(f.to_string()),
None => {
eprintln!(
"error: --export needs a format: czml, kml, geojson, stk, sigmf, all or list"
);
return ExitCode::from(2);
}
}
}
"--animate-duration" => {
i += 1;
match args.get(i).and_then(|v| v.parse::<f64>().ok()) {
Some(d) => {
animate_opts.duration_s = d;
animate_tuned = true;
}
None => {
eprintln!("error: --animate-duration needs a length in seconds");
return ExitCode::from(2);
}
}
}
"--import-route" => {
i += 1;
match args.get(i) {
Some(p) => route_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --import-route needs a GeoJSON file path");
return ExitCode::from(2);
}
}
}
"--eop" => {
i += 1;
match args.get(i) {
Some(p) => eop_path = Some(PathBuf::from(p)),
None => {
eprintln!("error: --eop needs a finals2000A file path");
return ExitCode::from(2);
}
}
}
other if other.starts_with('-') => {
eprintln!("error: unknown option '{other}'");
eprintln!("{USAGE}");
return ExitCode::from(2);
}
other if positional.is_none() => positional = Some(other.to_string()),
other => {
eprintln!("error: unexpected argument '{other}'; one scenario file per run");
eprintln!("{USAGE}");
return ExitCode::from(2);
}
}
i += 1;
}
if !animate_formats.is_empty() {
if let Err(e) = animate_opts.validate() {
eprintln!("error: {e}");
return ExitCode::from(2);
}
} else if animate_tuned {
eprintln!("warning: --animate-fps and --animate-duration do nothing without --animate");
}
if !animate_formats.is_empty() && (validate_path.is_some() || study_suite_path.is_some()) {
eprintln!("error: --animate exports one scenario run; it does not combine with --validate or --study");
return ExitCode::from(2);
}
if let Some(validate_file) = validate_path {
if positional.is_some() {
eprintln!("error: --validate lints one scenario; do not also pass a scenario file");
return ExitCode::from(2);
}
let src = match std::fs::read_to_string(&validate_file) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read {}: {e}", validate_file.display());
return ExitCode::FAILURE;
}
};
let violations = kshana::api::validate_scenario(&src);
if violations.is_empty() {
let kind = kshana::api::ScenarioKind::classify(&src)
.map(|k| k.as_str())
.unwrap_or("scenario");
let n_required = kshana::api::list_scenario_kinds()
.into_iter()
.find(|m| m.name == kind)
.map(|m| m.required_fields.len())
.unwrap_or(0);
if n_required == 0 {
println!(
"ok: {kind} — kind resolves; this kind requires no field (an empty body runs its reference configuration), so there was nothing further to check"
);
} else {
println!(
"ok: {kind} — kind resolves and all {n_required} of its published required-field entries are satisfied; field values are not checked"
);
}
return ExitCode::SUCCESS;
}
for v in &violations {
eprintln!("{v}");
}
return ExitCode::FAILURE;
}
if let Some(suite_path) = study_suite_path {
if positional.is_some() {
eprintln!("error: --study runs a suite; do not also pass a single scenario file");
return ExitCode::from(2);
}
if study_name.is_some() {
eprintln!(
"warning: --study-name is ignored with --study; a study is titled by the suite manifest's `title =`"
);
}
return run_study(&suite_path);
}
let Some(scenario_arg) = positional else {
eprintln!("{USAGE}");
return ExitCode::from(2);
};
let path = PathBuf::from(&scenario_arg);
let mut src = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read {}: {e}", path.display());
if e.kind() == std::io::ErrorKind::NotFound {
eprintln!(
"hint: `kshana example` lists the bundled reference scenarios; \
`kshana example clock-holdover > clock-holdover.toml` writes one"
);
}
return ExitCode::FAILURE;
}
};
let scenario_src = src.clone();
let mut report_extra_args: Vec<String> = Vec::new();
let mut report_input_files: Vec<(String, String)> = Vec::new();
if let Some(eop_file) = &eop_path {
let body = match std::fs::read_to_string(eop_file) {
Ok(b) => b,
Err(e) => {
eprintln!("error: cannot read {}: {e}", eop_file.display());
return ExitCode::FAILURE;
}
};
let eop_arg = eop_file.display().to_string();
report_extra_args.push("--eop".to_string());
report_extra_args.push(eop_arg.clone());
report_input_files.push((
eop_arg,
kshana::advanced_report::sha256_hex(body.as_bytes()),
));
match kshana::api::inject_eop(&src, &body) {
Ok(merged) => src = merged,
Err(e) => {
eprintln!("error: --eop: {e}");
return ExitCode::FAILURE;
}
}
}
if let Some(route_file) = &route_path {
let body = match std::fs::read_to_string(route_file) {
Ok(b) => b,
Err(e) => {
eprintln!("error: cannot read {}: {e}", route_file.display());
return ExitCode::FAILURE;
}
};
match kshana::interop::geojson::apply_route(&src, &body) {
Ok(merged) => src = merged,
Err(e) => {
eprintln!("error: --import-route: {e}");
return ExitCode::FAILURE;
}
}
}
let mut export_all = false;
let mut export_formats: Vec<kshana::interop::Format> = Vec::new();
for f in &exports {
match f.to_ascii_lowercase().as_str() {
"all" => export_all = true,
"list" => {
for (fmt, r) in kshana::interop::plan(&src) {
match r {
Ok(()) => println!("{}: applies", fmt.as_str()),
Err(why) => println!("{}: does not apply: {why}", fmt.as_str()),
}
}
return ExitCode::SUCCESS;
}
other => match kshana::interop::Format::parse(other) {
Ok(fmt) => {
if !export_formats.contains(&fmt) {
export_formats.push(fmt)
}
}
Err(e) => {
eprintln!("error: --export: {e}");
return ExitCode::from(2);
}
},
}
}
let mut out = match kshana::api::run_toml(&src) {
Ok(o) => o,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let output_base: PathBuf = match &study_name {
Some(name) => {
report_extra_args.push("--study-name".to_string());
report_extra_args.push(name.clone());
let meta = kshana::report::study_meta_with_title(name, &utc_iso8601_now());
out.json = kshana::api::with_study_meta(&out.json, &meta);
let dir = path.parent().unwrap_or_else(|| std::path::Path::new("."));
dir.join(kshana::report::slugify(name))
}
None => path.clone(),
};
let mut animations: Vec<(PathBuf, kshana::animation::Animation)> = Vec::new();
if !animate_formats.is_empty() {
let kind = kshana::api::ScenarioKind::classify(&src)
.map(|k| k.as_str().to_string())
.ok();
let mut written: Vec<String> = Vec::new();
for f in &animate_formats {
match kshana::animation::animate_result(&out.json, kind.as_deref(), *f, &animate_opts) {
Ok(a) => {
let target = match f {
kshana::animation::AnimationFormat::Svg => {
output_base.with_extension("animation.svg")
}
kshana::animation::AnimationFormat::Html => {
output_base.with_extension("animation.html")
}
kshana::animation::AnimationFormat::Frames => {
output_base.with_extension("frames")
}
};
written.push(
target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default(),
);
animations.push((target, a));
}
Err(e) => {
eprintln!("error: --animate {}: {e}", f.as_str());
return ExitCode::FAILURE;
}
}
}
match kshana::animation::animation_meta(
&out.json,
kind.as_deref(),
&animate_formats,
&animate_opts,
&written,
) {
Ok(meta) => out.json = kshana::animation::with_animation_meta(&out.json, &meta),
Err(e) => {
eprintln!("error: --animate: {e}");
return ExitCode::FAILURE;
}
}
}
let mut export_outputs: Vec<(PathBuf, Vec<u8>, String)> = Vec::new();
let mut export_skipped: Vec<String> = Vec::new();
let mut export_errors: Vec<String> = Vec::new();
let requested: Vec<(kshana::interop::Format, bool)> = if export_all {
kshana::interop::Format::ALL
.iter()
.map(|&f| (f, false))
.collect()
} else {
export_formats.iter().map(|&f| (f, true)).collect()
};
for (fmt, explicit) in requested {
match kshana::interop::export(&src, fmt) {
Ok(files) => {
for f in files {
let target = output_base.with_extension(f.suffix.trim_start_matches('.'));
export_outputs.push((target, f.bytes, fmt.as_str().to_string()));
}
}
Err(kshana::interop::ExportError::NotApplicable(why)) if !explicit => {
export_skipped.push(format!("skipped {}: {why}", fmt.as_str()));
}
Err(e) => export_errors.push(format!("error: {} export: {e}", fmt.as_str())),
}
}
let base_name = |p: &std::path::Path| {
p.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
};
let export_files: Vec<(String, String)> = export_outputs
.iter()
.map(|(t, _, f)| (f.clone(), base_name(t)))
.collect();
let animation_files: Vec<String> = animations.iter().map(|(t, _)| base_name(t)).collect();
if !export_files.is_empty() {
for (_, anim) in animations.iter_mut() {
if anim.format == kshana::animation::AnimationFormat::Html {
for f in anim.files.iter_mut() {
f.content = kshana::animation::with_companion_links(&f.content, &export_files);
}
}
}
}
let json_path = output_base.with_extension("result.json");
if let Err(e) = std::fs::write(&json_path, &out.json) {
eprintln!("error: cannot write {}: {e}", json_path.display());
return ExitCode::FAILURE;
}
let svg_path = output_base.with_extension("chart.svg");
if let Err(e) = std::fs::write(&svg_path, &out.svg) {
eprintln!("error: cannot write {}: {e}", svg_path.display());
return ExitCode::FAILURE;
}
let html_path = output_base.with_extension("report.html");
let report_json_path = output_base.with_extension("report.json");
let file_name =
|p: &std::path::Path| p.file_name().and_then(|n| n.to_str()).map(str::to_string);
let invocation = kshana::advanced_report::Invocation {
scenario_arg: scenario_arg.clone(),
extra_args: report_extra_args,
input_files: report_input_files,
result_file: file_name(&json_path),
chart_file: file_name(&svg_path),
animation_files,
export_files,
};
let report = match kshana::advanced_report::build(&out, &scenario_src, &invocation) {
Ok(r) => r,
Err(e) => {
eprintln!("error: cannot build the report: {e}");
return ExitCode::FAILURE;
}
};
if let Err(e) = std::fs::write(&html_path, report.to_html(&out.svg)) {
eprintln!("error: cannot write {}: {e}", html_path.display());
return ExitCode::FAILURE;
}
if let Err(e) = std::fs::write(&report_json_path, report.to_json()) {
eprintln!("error: cannot write {}: {e}", report_json_path.display());
return ExitCode::FAILURE;
}
let csv_path = output_base.with_extension("table.csv");
let wrote_csv = out.csv.is_some();
match out.write_csv(&csv_path) {
Ok(_) => {}
Err(e) => {
eprintln!("error: cannot write {}: {e}", csv_path.display());
return ExitCode::FAILURE;
}
}
println!("{}", out.summary);
if wrote_csv {
println!(
"wrote {}, {}, {}, {}, and {}",
json_path.display(),
svg_path.display(),
html_path.display(),
report_json_path.display(),
csv_path.display()
);
} else {
println!(
"wrote {}, {}, {}, and {}",
json_path.display(),
svg_path.display(),
html_path.display(),
report_json_path.display()
);
}
for (target, anim) in &animations {
match write_animation(target, anim) {
Ok(()) => println!("wrote {}", target.display()),
Err(e) => {
eprintln!("error: cannot write {}: {e}", target.display());
return ExitCode::FAILURE;
}
}
}
let sp3_target = match (&export_sp3_path, kshana::api::auto_export_sp3(&src)) {
(Some(p), _) => Some((p.clone(), kshana::api::export_sp3(&src))),
(None, Ok(Some(text))) => Some((path.with_extension("sp3"), Ok(text))),
(None, Ok(None)) => None,
(None, Err(e)) => Some((path.with_extension("sp3"), Err(e))),
};
if let Some((sp3_path, result)) = sp3_target {
match result {
Ok(text) => {
if let Err(e) = std::fs::write(&sp3_path, text) {
eprintln!("error: cannot write {}: {e}", sp3_path.display());
return ExitCode::FAILURE;
}
println!("wrote {}", sp3_path.display());
}
Err(e) => {
eprintln!("error: SP3 export failed: {e}");
return ExitCode::FAILURE;
}
}
}
let omm_target = match (&export_omm_path, kshana::api::auto_export_omm(&src)) {
(Some(p), _) => Some((p.clone(), kshana::api::export_omm(&src))),
(None, Ok(Some(text))) => Some((path.with_extension("omm"), Ok(text))),
(None, Ok(None)) => None,
(None, Err(e)) => Some((path.with_extension("omm"), Err(e))),
};
if let Some((omm_path, result)) = omm_target {
match result {
Ok(text) => {
if let Err(e) = std::fs::write(&omm_path, text) {
eprintln!("error: cannot write {}: {e}", omm_path.display());
return ExitCode::FAILURE;
}
println!("wrote {}", omm_path.display());
}
Err(e) => {
eprintln!("error: OMM export failed: {e}");
return ExitCode::FAILURE;
}
}
}
let oem_target = match (&export_oem_path, kshana::api::auto_export_oem(&src)) {
(Some(p), _) => Some((p.clone(), kshana::api::export_oem(&src))),
(None, Ok(Some(text))) => Some((path.with_extension("oem"), Ok(text))),
(None, Ok(None)) => None,
(None, Err(e)) => Some((path.with_extension("oem"), Err(e))),
};
if let Some((oem_path, result)) = oem_target {
match result {
Ok(text) => {
if let Err(e) = std::fs::write(&oem_path, text) {
eprintln!("error: cannot write {}: {e}", oem_path.display());
return ExitCode::FAILURE;
}
println!("wrote {}", oem_path.display());
}
Err(e) => {
eprintln!("error: OEM export failed: {e}");
return ExitCode::FAILURE;
}
}
}
for (target, bytes, _) in &export_outputs {
if let Err(e) = std::fs::write(target, bytes) {
eprintln!("error: cannot write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!("wrote {}", target.display());
}
for m in &export_skipped {
println!("{m}");
}
for m in &export_errors {
eprintln!("{m}");
}
if !export_errors.is_empty() {
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn write_animation(
target: &std::path::Path,
anim: &kshana::animation::Animation,
) -> std::io::Result<()> {
match anim.format {
kshana::animation::AnimationFormat::Frames => {
std::fs::create_dir_all(target)?;
for entry in std::fs::read_dir(target)? {
let entry = entry?;
let name = entry.file_name();
let name = name.to_string_lossy();
let is_frame = name.len() == "frame_0000.svg".len()
&& name.starts_with("frame_")
&& name.ends_with(".svg")
&& name[6..10].bytes().all(|b| b.is_ascii_digit());
if is_frame && entry.file_type()?.is_file() {
std::fs::remove_file(entry.path())?;
}
}
for f in &anim.files {
std::fs::write(target.join(&f.name), &f.content)?;
}
Ok(())
}
_ => match anim.files.first() {
Some(f) => std::fs::write(target, &f.content),
None => Ok(()),
},
}
}
fn print_kinds(rest: &[String]) -> ExitCode {
match rest {
[] => {
for meta in kshana::api::list_scenario_kinds() {
println!("{}", meta.name);
}
ExitCode::SUCCESS
}
[flag] if flag == "--json" => {
println!("{}", kshana::api::list_scenario_kinds_json());
ExitCode::SUCCESS
}
[other, ..] => {
eprintln!(
"error: unexpected argument '{other}' after `kinds`; only --json is accepted"
);
ExitCode::from(2)
}
}
}
fn print_example(rest: &[String]) -> ExitCode {
match rest {
[] => {
eprintln!(
"Bundled reference scenarios ({}). Write one to a file and run it:\n \
kshana example clock-holdover > clock-holdover.toml\n \
kshana clock-holdover.toml",
bundled_scenarios::BUNDLED.len()
);
for (name, _) in bundled_scenarios::BUNDLED {
println!("{name}");
}
for (name, _) in bundled_scenarios::REPO_ONLY {
eprintln!("(not bundled: {name} — run `kshana example {name}` for why)");
}
ExitCode::SUCCESS
}
[name] => {
if let Some(toml) = bundled_scenarios::get(name) {
print!("{toml}");
return ExitCode::SUCCESS;
}
if let Some(why) = bundled_scenarios::repo_only_reason(name) {
eprintln!("error: '{name}' is not bundled: it {why}");
return ExitCode::from(2);
}
eprintln!(
"error: no bundled scenario named '{name}'; `kshana example` lists the {} that are",
bundled_scenarios::BUNDLED.len()
);
ExitCode::from(2)
}
[_, extra, ..] => {
eprintln!(
"error: unexpected argument '{extra}' after `example <name>`; it takes one name"
);
ExitCode::from(2)
}
}
}
fn run_study(suite_path: &std::path::Path) -> ExitCode {
let src = match std::fs::read_to_string(suite_path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read {}: {e}", suite_path.display());
return ExitCode::FAILURE;
}
};
let suite = match kshana::suite::parse_suite(&src) {
Ok(s) => s,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let base_dir = suite_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let out = match kshana::study::run_suite(&suite, base_dir) {
Ok(o) => o,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let json = stamp_study_generated(&out.json, &utc_iso8601_now());
let base = output_base_for_suite(suite_path);
let json_path = base.with_extension("study.json");
if let Err(e) = std::fs::write(&json_path, &json) {
eprintln!("error: cannot write {}: {e}", json_path.display());
return ExitCode::FAILURE;
}
let html_path = base.with_extension("study.html");
if let Err(e) = std::fs::write(&html_path, &out.html) {
eprintln!("error: cannot write {}: {e}", html_path.display());
return ExitCode::FAILURE;
}
println!("{}", out.summary);
println!("wrote {} and {}", json_path.display(), html_path.display());
ExitCode::SUCCESS
}
fn output_base_for_suite(suite_path: &std::path::Path) -> PathBuf {
suite_path.with_extension("")
}
fn stamp_study_generated(json: &str, stamp: &str) -> String {
match serde_json::from_str::<serde_json::Value>(json) {
Ok(mut v) => {
if let Some(obj) = v.as_object_mut() {
obj.insert(
"generated_utc".to_string(),
serde_json::Value::String(stamp.to_string()),
);
}
serde_json::to_string_pretty(&v).unwrap_or_else(|_| json.to_string())
}
Err(_) => json.to_string(),
}
}
fn run_bench_export_cli(args: &[String]) -> ExitCode {
let mut path: Option<PathBuf> = None;
let mut out: Option<String> = None;
let mut epoch: Option<kshana::interop::UtcEpoch> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--out" | "--epoch" if i + 1 >= args.len() => {
eprintln!("error: {} needs a value", args[i]);
return ExitCode::from(2);
}
"--out" => {
out = Some(args[i + 1].clone());
i += 1;
}
"--epoch" => {
match kshana::interop::UtcEpoch::parse_iso(&args[i + 1]) {
Ok(e) => epoch = Some(e),
Err(e) => {
eprintln!("error: --epoch: {e}");
return ExitCode::from(2);
}
}
i += 1;
}
a if a.starts_with("--") => {
eprintln!("error: unknown bench-export option {a}");
return ExitCode::from(2);
}
a => path = Some(PathBuf::from(a)),
}
i += 1;
}
let Some(path) = path else {
eprintln!("error: bench-export needs a scenario path");
return ExitCode::from(2);
};
let src = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read {}: {e}", path.display());
return ExitCode::from(2);
}
};
let (files, notes) = match kshana::interop::testbench::export_with_notes(&src, epoch) {
Ok(f) => f,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let base = out.unwrap_or_else(|| path.with_extension("").display().to_string());
for f in files {
let target = PathBuf::from(format!("{base}{}", f.suffix));
if let Err(e) = std::fs::write(&target, &f.bytes) {
eprintln!("error: cannot write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!("wrote {}", target.display());
}
for n in notes {
eprintln!("note: {n}");
}
ExitCode::SUCCESS
}
fn run_compliance_report_cli(args: &[String]) -> ExitCode {
use kshana::compliance::{self, mapping};
let mut out = String::from("compliance-report");
let mut paths: Vec<PathBuf> = Vec::new();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--mapping" => {
println!("> {}\n", compliance::STATEMENT);
for fw in mapping::Framework::ALL {
println!("## {}\n\n{}", fw.title(), mapping::framework_table_md(fw));
}
return ExitCode::SUCCESS;
}
"--sources" => {
println!("> {}\n", compliance::STATEMENT);
for fw in mapping::Framework::ALL {
println!("## {}\n\n{}", fw.title(), mapping::sources_md(fw));
}
return ExitCode::SUCCESS;
}
"--out" if i + 1 < args.len() => {
out = args[i + 1].clone();
i += 1;
}
"--out" => {
eprintln!("error: --out needs a value");
return ExitCode::from(2);
}
a if a.starts_with("--") => {
eprintln!("error: unknown compliance-report option {a}");
return ExitCode::from(2);
}
a => paths.push(PathBuf::from(a)),
}
i += 1;
}
if paths.is_empty() {
eprintln!("error: compliance-report needs at least one result .json file");
return ExitCode::from(2);
}
let (runs, bad) = compliance::load_runs(&paths);
let report = compliance::assess(&runs, bad);
for (ext, body) in [
("compliance.md", report.to_markdown()),
("compliance.json", report.to_json()),
] {
let target = PathBuf::from(format!("{out}.{ext}"));
if let Err(e) = std::fs::write(&target, body) {
eprintln!("error: cannot write {}: {e}", target.display());
return ExitCode::FAILURE;
}
println!("wrote {}", target.display());
}
let n = |s: compliance::Status| report.rows.iter().filter(|r| r.status == s).count();
println!(
"{} runs read, {} not used; rows: {} evidenced, {} partly, {} not evidenced, {} out of scope",
runs.len(),
report.unrecognised.len(),
n(compliance::Status::Evidenced),
n(compliance::Status::PartlyEvidenced),
n(compliance::Status::NotEvidenced),
n(compliance::Status::OutOfScope)
);
ExitCode::SUCCESS
}
fn run_receiver_trust_cli(args: &[String]) -> ExitCode {
if args.first().map(String::as_str) == Some("live") {
return live_io::run(&args[1..]);
}
let Some(path) = args.first() else {
eprintln!("error: receiver-trust needs a scenario path");
return ExitCode::from(2);
};
let path = PathBuf::from(path);
let src = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read {}: {e}", path.display());
return ExitCode::from(2);
}
};
let mut scn: kshana::receiver_trust::scenario::ReceiverTrustScenario =
match toml::from_str(&src) {
Ok(s) => s,
Err(e) => {
eprintln!("error: invalid receiver-trust scenario: {e}");
return ExitCode::from(2);
}
};
let base = path.parent().map(PathBuf::from).unwrap_or_default();
kshana::receiver_trust::scenario::resolve_paths(&mut scn, &base);
let out = match kshana::receiver_trust::scenario::run_scenario(&scn) {
Ok(o) => o,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
let stem = path.with_extension("");
for (ext, body) in [
("result.json", &out.json),
("trust.csv", &out.csv),
("trust.svg", &out.svg),
] {
let target = PathBuf::from(format!("{}.{ext}", stem.display()));
if let Err(e) = std::fs::write(&target, body) {
eprintln!("error: cannot write {}: {e}", target.display());
return ExitCode::FAILURE;
}
}
println!("{}", out.summary);
ExitCode::SUCCESS
}