pub const FILE_SOURCE_KEYS: &[&str] = &[
"csv_path",
"data_dir",
"data_path",
"dem_hgt",
"dem_path",
"dem_tile_path",
"earth_orientation_kernel_path",
"ephemeris_path",
"gfc_path",
"meta_path",
"moon_orientation_kernel_path",
"normal_points_dir",
"planetary_kernel_path",
"reflectors_path",
"stations_path",
];
pub const FILE_SOURCE_KEYS_BY_KIND: &[(&str, &[&str])] = &[
(
"realtime-frame-eop",
&["eop_finals2000a", "eop_finals2000a_later"],
),
("time-transfer-budget", &["paths"]),
];
pub const FILE_READING_KINDS: &[&str] = &["lunar-llr-datum"];
const FILE_SOURCE_TABLES: &[&str] = &["log", "nav", "clock_log"];
pub fn reject_file_sources(toml_text: &str, max_bytes: usize) -> Result<(), String> {
check(toml_text, max_bytes, true)
}
pub fn reject_file_fields(toml_text: &str, max_bytes: usize) -> Result<(), String> {
check(toml_text, max_bytes, false)
}
fn check(toml_text: &str, max_bytes: usize, refuse_file_reading_kinds: bool) -> Result<(), String> {
if toml_text.len() > max_bytes {
return Err(format!(
"scenario is {} bytes, over the {max_bytes}-byte limit",
toml_text.len()
));
}
let Ok(v) = toml_text.parse::<toml::Table>() else {
return Ok(());
};
fn walk(
t: &toml::Table,
kind: &str,
table_name: &str,
refuse_kinds: bool,
) -> Result<(), String> {
let kind = t.get("kind").and_then(toml::Value::as_str).unwrap_or(kind);
if refuse_kinds && FILE_READING_KINDS.contains(&kind) {
return Err(format!(
"the scenario kind `{kind}` reads its data from files on the host, so this surface does \
not run it; use the `kshana` command line"
));
}
for (k, v) in t {
let by_kind = FILE_SOURCE_KEYS_BY_KIND
.iter()
.any(|(kd, keys)| *kd == kind && keys.contains(&k.as_str()));
let in_table = k == "path" && FILE_SOURCE_TABLES.contains(&table_name);
if FILE_SOURCE_KEYS.contains(&k.as_str()) || by_kind || in_table {
return Err(format!(
"the field `{k}` names a file: this surface accepts inline content only"
));
}
match v {
toml::Value::Table(inner) => walk(inner, kind, k, refuse_kinds)?,
toml::Value::Array(items) => {
for item in items {
if let toml::Value::Table(inner) = item {
walk(inner, kind, k, refuse_kinds)?;
}
}
}
_ => {}
}
}
Ok(())
}
walk(&v, "", "", refuse_file_reading_kinds)
}
#[cfg(test)]
mod tests {
use super::*;
const MAX: usize = 4 * 1024 * 1024;
#[test]
fn scenarios_naming_files_are_refused() {
let refused = |t: &str| reject_file_sources(t, MAX).unwrap_err();
assert!(refused("kind = \"telecom-timing\"\ncsv_path = \"x.csv\"").contains("csv_path"));
assert!(
refused("kind = \"spectrum\"\n[recording]\nmeta_path = \"a.sigmf-meta\"")
.contains("meta_path")
);
assert!(
refused("kind = \"realtime-frame-eop\"\neop_finals2000a = \"x\"")
.contains("eop_finals2000a")
);
assert!(
refused("kind = \"receiver-trust\"\n[log]\nformat = \"nmea\"\npath = \"x\"")
.contains("path")
);
assert!(refused("[log.nav]\npath = \"x\"").contains("path"));
assert!(refused(
"kind = \"time-transfer-budget\"\n[[series]]\nformat = \"mjd-csv\"\npaths = [\"x.csv\"]"
)
.contains("paths"));
assert!(refused(
"kind = \"telecom-timing\"\n[clock_log]\nformat = \"ticc\"\npath = \"x.txt\""
)
.contains("path"));
assert!(
refused("kind = \"clock\"\n[clock_log]\nformat = \"timelab\"\npath = \"x\"")
.contains("path")
);
assert!(refused(
"kind = \"campaign\"\n[[phases]]\n[phases.scenario]\nplanetary_kernel_path = \"k\""
)
.contains("planetary_kernel_path"));
}
#[test]
fn a_kind_that_reads_a_default_data_directory_is_refused_generically() {
let e = reject_file_sources("kind = \"lunar-llr-datum\"\nseed = 1", MAX).unwrap_err();
assert!(
e.contains("lunar-llr-datum") && e.contains("command line"),
"{e}"
);
assert!(
!e.contains("tests/") && !e.contains("fixtures") && !e.contains("No such file"),
"{e}"
);
reject_file_fields("kind = \"lunar-llr-datum\"", MAX).unwrap();
assert!(reject_file_sources(
"kind = \"campaign\"\n[[phases]]\n[phases.scenario]\nkind = \"lunar-llr-datum\"",
MAX
)
.is_err());
}
#[test]
fn inline_bodies_result_paths_and_non_toml_pass() {
reject_file_sources(
"kind = \"ephemeris\"\neop_finals2000a = \"inline body\"",
MAX,
)
.unwrap();
reject_file_sources(
"kind = \"campaign\"\n[[metrics]]\nname = \"a\"\npath = \"quantum.fom.x\"",
MAX,
)
.unwrap();
reject_file_sources(
"kind = \"telecom-timing\"\n[clock_log]\nformat = \"ticc\"\ntext = \"inline\"",
MAX,
)
.unwrap();
reject_file_sources("not toml {{", MAX).unwrap();
assert!(reject_file_sources("x = 1", 2)
.unwrap_err()
.contains("limit"));
}
}