oxide_cli/addons/steps/
mod.rs1use std::path::{Component, Path, PathBuf};
2
3use anyhow::{Context, Result};
4
5pub mod append;
6pub mod copy;
7pub mod create;
8pub mod delete;
9pub mod inject;
10pub mod move_step;
11pub mod rename;
12pub mod replace;
13
14pub enum Rollback {
15 DeleteCreatedFile { path: PathBuf },
16 RestoreFile { path: PathBuf, original: Vec<u8> },
17 RenameFile { from: PathBuf, to: PathBuf },
18}
19
20pub fn render_lines(lines: &[String], ctx: &tera::Context) -> Result<Vec<String>> {
22 lines
23 .iter()
24 .map(|line| tera::Tera::one_off(line, ctx, false).map_err(Into::into))
25 .collect()
26}
27
28fn normalize_join(root: &Path, relative: &str) -> PathBuf {
31 let joined = root.join(relative);
32 let mut out = PathBuf::new();
33 for component in joined.components() {
34 match component {
35 Component::ParentDir => {
36 out.pop();
37 }
38 Component::CurDir => {}
39 c => out.push(c),
40 }
41 }
42 out
43}
44
45fn normalize_path(path: &Path) -> PathBuf {
47 let mut out = PathBuf::new();
48 for component in path.components() {
49 match component {
50 Component::ParentDir => {
51 out.pop();
52 }
53 Component::CurDir => {}
54 c => out.push(c),
55 }
56 }
57 out
58}
59
60pub(super) fn safe_join(root: &Path, relative: &str, label: &str) -> Result<PathBuf> {
66 let norm_root = normalize_path(root);
67 let path = normalize_join(root, relative);
68 if !path.starts_with(&norm_root) {
69 return Err(anyhow::anyhow!(
70 "Path traversal blocked: {} '{}' would escape the root directory",
71 label,
72 relative
73 ));
74 }
75 Ok(path)
76}
77
78pub(super) fn resolve_target(
79 target: &crate::addons::manifest::Target,
80 project_root: &Path,
81) -> Result<Vec<PathBuf>> {
82 use crate::addons::manifest::Target;
83 match target {
84 Target::File { file } => {
85 let path = safe_join(project_root, file, "target file")?;
86 Ok(vec![path])
87 }
88 Target::Glob { glob } => {
89 safe_join(project_root, glob, "glob pattern")?;
91 let pattern = project_root.join(glob).to_string_lossy().to_string();
92 let canonical_root = project_root
93 .canonicalize()
94 .with_context(|| format!("Cannot resolve project root '{}'", project_root.display()))?;
95 let paths = glob::glob(&pattern)?
96 .filter_map(|e| e.ok())
97 .filter(|p| {
99 p.canonicalize()
100 .map(|cp| cp.starts_with(&canonical_root))
101 .unwrap_or(false)
102 })
103 .collect();
104 Ok(paths)
105 }
106 }
107}