mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
security(export): confine metadata-derived names (#2903)
This commit is contained in:
@@ -9,6 +9,16 @@ use serde_json::{Value, json};
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn project_names_become_one_export_filename_component() {
|
||||
for name in ["../../outside", "/absolute/path", r"C:\\outside", ".."] {
|
||||
let safe = crate::export_tools::file_safe(name);
|
||||
assert!(!safe.contains(['/', '\\']), "{name:?} became {safe:?}");
|
||||
assert_ne!(safe, ".");
|
||||
assert_ne!(safe, "..");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_export_ranges_are_rejected_without_panicking() {
|
||||
let mut session = demo();
|
||||
|
||||
@@ -385,10 +385,11 @@ pub fn default_export_dir(s: &Session) -> PathBuf {
|
||||
crate::temp_dir()
|
||||
}
|
||||
|
||||
fn file_safe(n: &str) -> String {
|
||||
/// Convert project-derived display text into exactly one safe output filename component.
|
||||
pub fn file_safe(n: &str) -> String {
|
||||
let s: String = n.chars().map(|c| if c.is_alphanumeric() || " -_().".contains(c) { c } else { '_' }).collect();
|
||||
let s = s.trim().to_string();
|
||||
if s.is_empty() { "Export".into() } else { s }
|
||||
if s.is_empty() || matches!(s.as_str(), "." | "..") { "Export".into() } else { s }
|
||||
}
|
||||
|
||||
/// `path` with the settings' extension (added when missing; image sequences keep theirs).
|
||||
|
||||
@@ -1414,7 +1414,9 @@ fn quick_file(app: &FilmcraftApp, path: &str, seq_name: &str, ext: &str) -> Opti
|
||||
return None;
|
||||
}
|
||||
let file = match name {
|
||||
None => format!("{seq_name}.{ext}"),
|
||||
// Sequence names come from the project, not from this Quick Export interaction. Keep
|
||||
// metadata in one filename component before joining it to the selected/default folder.
|
||||
None => format!("{}.{}", filmcraft_engine::export_tools::file_safe(seq_name), ext),
|
||||
Some(n) if cur.eq_ignore_ascii_case(ext) => n.to_string(),
|
||||
Some(n) if !cur.is_empty() && app.session.export_presets.all().iter().any(|x| x.settings.extension().eq_ignore_ascii_case(cur)) => {
|
||||
std::path::Path::new(n).with_extension(ext).to_string_lossy().to_string()
|
||||
|
||||
Reference in New Issue
Block a user