mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 08:51:30 -06:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9664c01289 |
@@ -755,10 +755,10 @@ thread_local! {
|
||||
/// While one is alive, [`read_file`] and [`write_file`] on this thread refuse every path: a
|
||||
/// stored script's steps run inside one (#2178), so no command reaches the file system from a
|
||||
/// script whatever its parameters are called. Dropping it (also on unwind) lifts the refusal.
|
||||
pub(crate) struct ScriptSandbox(());
|
||||
pub struct ScriptSandbox(());
|
||||
|
||||
impl ScriptSandbox {
|
||||
pub(crate) fn enter() -> Self {
|
||||
pub fn enter() -> Self {
|
||||
let _ = SCRIPT_SANDBOX.try_with(|c| c.set(c.get().saturating_add(1)));
|
||||
ScriptSandbox(())
|
||||
}
|
||||
|
||||
+1
-10
@@ -470,14 +470,6 @@ impl Import {
|
||||
None => return Err(IoError::TooLarge("ODS row exceeds worksheet width".into())),
|
||||
};
|
||||
if kept {
|
||||
// Charge the text a row keeps before the row is done, so a row of many long cells is
|
||||
// refused as it grows; finish_row charges the row's repeats beyond the first.
|
||||
if let Value::Text(t) = &cell.value {
|
||||
self.text = self.text.saturating_add(u64::from(draft.repeat).saturating_mul(t.len() as u64));
|
||||
if self.text > MAX_TEXT {
|
||||
return Err(IoError::TooLarge("ODS expands beyond the import limit (64 MiB of text)".into()));
|
||||
}
|
||||
}
|
||||
row.cells.push(RepeatedCell { col: row.col, repeat: draft.repeat, width, height, cell });
|
||||
}
|
||||
row.col = end;
|
||||
@@ -545,8 +537,7 @@ impl Import {
|
||||
if !entry.cell.is_blank() {
|
||||
self.cells = self.cells.saturating_add(count);
|
||||
if let Value::Text(t) = &entry.cell.value {
|
||||
let repeats = count.saturating_sub(u64::from(entry.repeat));
|
||||
self.text = self.text.saturating_add(repeats.saturating_mul(t.len() as u64));
|
||||
self.text = self.text.saturating_add(count.saturating_mul(t.len() as u64));
|
||||
}
|
||||
}
|
||||
let merged = entry.width > 1 || entry.height > 1;
|
||||
|
||||
@@ -200,18 +200,6 @@ fn ods_expansion_bounds_stop_small_repeat_bombs() {
|
||||
"x".repeat(1024)
|
||||
);
|
||||
assert!(matches!(read_ods(&ods(&text)), Err(IoError::TooLarge(_))));
|
||||
// One row of cells each just under the 1 MiB cell limit, past 64 MiB in all: refused while
|
||||
// the row is read, before it holds them all. The row's end is never reached.
|
||||
let long = format!(r#"<table:table-cell office:value-type="string" office:string-value="{}"/>"#, "x".repeat(1024 * 1024 - 1));
|
||||
let cells = ods(&format!(r#"<table:table table:name="Data"><table:table-row>{}"#, long.repeat(65)));
|
||||
assert!(matches!(read_ods(&cells), Err(IoError::TooLarge(_))));
|
||||
// A row repeated once more than its cells' text allows is refused too, and one that fits reads.
|
||||
let cell = format!(r#"<table:table-cell office:value-type="string" office:string-value="{}"/>"#, "y".repeat(1024));
|
||||
let rows = |n: u32| {
|
||||
ods(&format!(r#"<table:table table:name="Data"><table:table-row table:number-rows-repeated="{n}">{cell}</table:table-row></table:table>"#))
|
||||
};
|
||||
assert!(read_ods(&rows(65_536)).is_ok());
|
||||
assert!(matches!(read_ods(&rows(65_537)), Err(IoError::TooLarge(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+43
-9
@@ -77,6 +77,8 @@ fn probe_live_commands(calls: &[(String, String)]) -> Probe {
|
||||
let params = if params.is_empty() { serde_json::json!({}) } else { serde_json::from_str(params).unwrap_or_default() };
|
||||
let mut s = Session::new();
|
||||
s.new_workbook();
|
||||
// The JSON picks the commands and their params: none of them may touch the file system.
|
||||
let _sandbox = gridcraft_engine::io::ScriptSandbox::enter();
|
||||
let status = match s.execute(id, params.clone()) {
|
||||
Ok(_) => {
|
||||
if s.journal.iter().any(|(jid, _)| jid == id) {
|
||||
@@ -181,15 +183,16 @@ fn parse_and_probe(source: &str) -> Result<(Vec<Entry>, Probe), String> {
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.push(Entry {
|
||||
seq,
|
||||
kind,
|
||||
target,
|
||||
pending,
|
||||
params,
|
||||
excel: e.get("excel").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||
note: e.get("note").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
||||
});
|
||||
let excel = e.get("excel").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let note = e.get("note").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
// These are written into generated Rust: a target inside string literals and format
|
||||
// strings, the annotations after `//`. Refuse anything that could end either.
|
||||
for (field, text) in [("target", &target), ("excel", &excel), ("note", ¬e)] {
|
||||
if text.chars().any(|c| c.is_control() || matches!(c, '"' | '\\' | '{' | '}')) {
|
||||
return Err(format!("{at}: `{field}` must not hold quotes, backslashes, braces or control characters: {text:?}"));
|
||||
}
|
||||
}
|
||||
entries.push(Entry { seq, kind, target, pending, params, excel, note });
|
||||
}
|
||||
// JSON order is authoritative and curated: the first entry for a tab is its canonical
|
||||
// badge (legacy aliases come later), and groups stay together. Do not sort.
|
||||
@@ -434,3 +437,34 @@ fn reset(h: &mut Harness) {
|
||||
key(h, Key::Escape); // leave keytip mode entirely
|
||||
}
|
||||
"#;
|
||||
|
||||
#[cfg(all(test, feature = "engine"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Text that would end a comment or string literal in the generated Rust is refused.
|
||||
#[test]
|
||||
fn annotations_and_targets_cannot_escape_generated_code() {
|
||||
for entry in [
|
||||
r#"{"seq": "Q", "kind": "command", "target": "edit.copy", "note": "x\nfn evil() {}"}"#,
|
||||
r#"{"seq": "Q", "kind": "command", "target": "edit.copy", "excel": "a\rb"}"#,
|
||||
r#"{"seq": "Q", "kind": "dialog", "target": "x\"); evil(); (\""}"#,
|
||||
r#"{"seq": "Q", "kind": "control", "target": "{0}"}"#,
|
||||
] {
|
||||
let doc = format!(r#"{{"sequences": [{entry}]}}"#);
|
||||
assert!(parse_and_probe(&doc).is_err(), "{entry}");
|
||||
}
|
||||
assert!(parse_and_probe(r#"{"sequences": [{"seq": "Q", "kind": "control", "target": "fontSize", "note": "Font Size box"}]}"#).is_ok());
|
||||
}
|
||||
|
||||
/// A probed command may not write files, whatever its params say.
|
||||
#[test]
|
||||
fn probes_cannot_touch_the_file_system() {
|
||||
let path = std::env::temp_dir().join(format!("keytips-probe-{}.pdf", std::process::id()));
|
||||
let _ = fs::remove_file(&path);
|
||||
let calls = vec![("file.exportPdf".to_string(), serde_json::json!({ "path": path.to_string_lossy() }).to_string())];
|
||||
let probe = probe_live_commands(&calls);
|
||||
assert_eq!(probe.values().copied().collect::<Vec<_>>(), [2]);
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user