From 73817dc3f0c37ccbf639add2f54925322dc40bea Mon Sep 17 00:00:00 2001 From: Krueels <90205419+Krueels@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:28:04 -0300 Subject: [PATCH] #986: confine Blender exports and trim generated header --- crates/automation/src/confine.rs | 1 + crates/automation/src/tests.rs | 27 ++++++++++++++++++++++++++ crates/engine/src/cmd/blender_scene.py | 3 +-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/automation/src/confine.rs b/crates/automation/src/confine.rs index 67e5aef98..dc64084d3 100644 --- a/crates/automation/src/confine.rs +++ b/crates/automation/src/confine.rs @@ -97,6 +97,7 @@ const MIXED_WRITES: &[(&str, &[&str])] = &[("templates.create", &["projectPath", /// Commands that write files named by these parameters. const WRITES: &[(&str, &[&str])] = &[ + ("camera.exportBlender", &["path"]), ("file.save", &["path"]), ("file.saveAs", &["path"]), ("file.saveCopy", &["path"]), diff --git a/crates/automation/src/tests.rs b/crates/automation/src/tests.rs index 560a527c7..49f0389f6 100644 --- a/crates/automation/src/tests.rs +++ b/crates/automation/src/tests.rs @@ -1473,3 +1473,30 @@ fn automation_roots_refuse_running_script_code() { call_json(&mut s, "execute_command", json!({"command": "scriptui.list"})); let _ = std::fs::remove_dir_all(&base); } + +#[test] +fn blender_export_requires_an_absolute_path_in_a_write_root() { + use crate::confine::Confinement; + let base = std::env::temp_dir().join(format!("ec-blender-confine-{}", std::process::id())); + let (read, write) = (base.join("read"), base.join("write")); + for dir in [&read, &write] { + std::fs::create_dir_all(dir).unwrap(); + } + let confinement = Confinement::new(std::slice::from_ref(&read), std::slice::from_ref(&write)).unwrap(); + let check = |path: &std::path::Path| confinement.check_command("camera.exportBlender", &json!({"path": path})); + assert_eq!(check(&write.join("scene.py")).unwrap(), None); + for path in [read.join("scene.py"), base.join("outside.py"), write.join("../read/scene.py")] { + assert!(check(&path).is_err(), "must deny {}", path.display()); + } + for path in ["scene.py", "./scene.py", "write/scene.py", "../scene.py"] { + let error = check(std::path::Path::new(path)).unwrap_err().to_string(); + assert!(error.contains("give an absolute path"), "{path}: {error}"); + } + let read_only = Confinement::new(std::slice::from_ref(&write), &[]).unwrap(); + assert!(read_only.check_command("camera.exportBlender", &json!({"path": write.join("scene.py")})).is_err()); + for path in [json!(read.join("scene.py")), json!("scene.py"), json!("$1.path")] { + let batch = json!({"steps": [{"command": "camera.exportBlender", "params": {"path": path}}]}); + assert!(confinement.check_command("engine.batch", &batch).is_err()); + } + std::fs::remove_dir_all(&base).unwrap(); +} diff --git a/crates/engine/src/cmd/blender_scene.py b/crates/engine/src/cmd/blender_scene.py index 9f8a522c5..0a9700a00 100644 --- a/crates/engine/src/cmd/blender_scene.py +++ b/crates/engine/src/cmd/blender_scene.py @@ -2,8 +2,7 @@ # Run in Blender's Scripting workspace (Open, Run Script), or: # blender --background --python "Tracked Scene.py" # Creates a NEW scene; existing scenes and objects are preserved. Save the .blend yourself. -# Public API references: https://docs.blender.org/api/4.5/bpy.types.Camera.html -# https://docs.blender.org/api/4.5/bpy.types.ImageUser.html +# Public API types: bpy.types.Camera, bpy.types.ImageUser import json import math import os