diff --git a/README.md b/README.md index 8d01532..ca15cfc 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ This section is for contributors and anyone building on the project. ## Rulesets 🧩 Rules are data‑driven and split by domain in `rulesets/*.toml`. +When running an installed binary, the first run will auto‑install the default rulesets +into the XDG data directory (for example `~/.local/share/vid-repair/rulesets` on Linux). Before shipping changes: ```bash diff --git a/vid-repair-core/src/rules/embedded.rs b/vid-repair-core/src/rules/embedded.rs new file mode 100644 index 0000000..8985e4e --- /dev/null +++ b/vid-repair-core/src/rules/embedded.rs @@ -0,0 +1,45 @@ +pub const FILES: &[(&str, &str)] = &[ + ( + "containers.toml", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rulesets/containers.toml" + )), + ), + ( + "decode.toml", + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../rulesets/decode.toml")), + ), + ( + "probe.toml", + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../rulesets/probe.toml")), + ), + ( + "codecs-video.toml", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rulesets/codecs-video.toml" + )), + ), + ( + "codecs-audio.toml", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rulesets/codecs-audio.toml" + )), + ), + ( + "transport.toml", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rulesets/transport.toml" + )), + ), + ( + "timestamps.toml", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../rulesets/timestamps.toml" + )), + ), +]; diff --git a/vid-repair-core/src/rules/mod.rs b/vid-repair-core/src/rules/mod.rs index 8d5cd78..9238c3a 100644 --- a/vid-repair-core/src/rules/mod.rs +++ b/vid-repair-core/src/rules/mod.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; use anyhow::Result; +use directories::ProjectDirs; +use fs_err as fs; use crate::scan::ProbeData; @@ -8,6 +10,7 @@ mod loader; mod lint; mod matcher; mod model; +mod embedded; use model::Rule; @@ -22,23 +25,14 @@ pub struct RuleSet { impl RuleSet { pub fn load() -> Result { - let mut candidates = Vec::new(); - - if let Ok(current) = std::env::current_dir() { - candidates.push(current.join("rulesets")); + if let Some(ruleset) = load_compiled_from_candidates()? { + return Ok(ruleset); } - if let Ok(exe) = std::env::current_exe() { - if let Some(parent) = exe.parent() { - candidates.push(parent.join("rulesets")); - } - } - - for dir in candidates { - let rules = loader::load_rules_from_dir(&dir)?; - if !rules.is_empty() { - let compiled = loader::compile_rules(rules)?; - return Ok(Self { rules: compiled }); + if let Some(dir) = data_ruleset_dir() { + install_embedded_rulesets(&dir)?; + if let Some(ruleset) = load_compiled_from_candidates()? { + return Ok(ruleset); } } @@ -72,21 +66,13 @@ impl RuleSet { } pub fn load_raw_rules() -> Result> { - let mut candidates = Vec::new(); - - if let Ok(current) = std::env::current_dir() { - candidates.push(current.join("rulesets")); + if let Some(rules) = load_raw_from_candidates()? { + return Ok(rules); } - if let Ok(exe) = std::env::current_exe() { - if let Some(parent) = exe.parent() { - candidates.push(parent.join("rulesets")); - } - } - - for dir in candidates { - let rules = loader::load_rules_from_dir(&dir)?; - if !rules.is_empty() { + if let Some(dir) = data_ruleset_dir() { + install_embedded_rulesets(&dir)?; + if let Some(rules) = load_raw_from_candidates()? { return Ok(rules); } } @@ -114,17 +100,14 @@ pub fn build_context(probe: &ProbeData) -> RuleContext { } pub fn ruleset_dir_for_display() -> Result { - if let Ok(current) = std::env::current_dir() { - let dir = current.join("rulesets"); + for dir in candidate_ruleset_dirs() { if dir.exists() { return Ok(dir); } } - if let Ok(exe) = std::env::current_exe() { - if let Some(parent) = exe.parent() { - return Ok(parent.join("rulesets")); - } + if let Some(dir) = data_ruleset_dir() { + return Ok(dir); } Err(anyhow::anyhow!("No ruleset directory found")) @@ -140,3 +123,74 @@ pub fn ensure_ruleset_loaded(ruleset: &RuleSet) -> Result<()> { } Ok(()) } + +fn candidate_ruleset_dirs() -> Vec { + let mut candidates = Vec::new(); + + if let Ok(current) = std::env::current_dir() { + candidates.push(current.join("rulesets")); + } + + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + candidates.push(parent.join("rulesets")); + } + } + + if let Some(dir) = data_ruleset_dir() { + candidates.push(dir); + } + + if let Some(dir) = config_ruleset_dir() { + candidates.push(dir); + } + + candidates +} + +fn data_ruleset_dir() -> Option { + ProjectDirs::from("cc", "44r0n", "vid-repair").map(|dirs| dirs.data_dir().join("rulesets")) +} + +fn config_ruleset_dir() -> Option { + ProjectDirs::from("cc", "44r0n", "vid-repair").map(|dirs| dirs.config_dir().join("rulesets")) +} + +fn load_compiled_from_candidates() -> Result> { + for dir in candidate_ruleset_dirs() { + let rules = loader::load_rules_from_dir(&dir)?; + if !rules.is_empty() { + let compiled = loader::compile_rules(rules)?; + return Ok(Some(RuleSet { rules: compiled })); + } + } + Ok(None) +} + +fn load_raw_from_candidates() -> Result>> { + for dir in candidate_ruleset_dirs() { + let rules = loader::load_rules_from_dir(&dir)?; + if !rules.is_empty() { + return Ok(Some(rules)); + } + } + Ok(None) +} + +fn install_embedded_rulesets(dir: &std::path::Path) -> Result<()> { + if dir.exists() && dir.is_file() { + return Ok(()); + } + + fs::create_dir_all(dir)?; + + for (name, contents) in embedded::FILES { + let path = dir.join(name); + if path.exists() { + continue; + } + fs::write(&path, contents)?; + } + + Ok(()) +}