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
28pub fn render_string(s: &str, ctx: &tera::Context) -> Result<String> {
30 tera::Tera::one_off(s, ctx, false).map_err(Into::into)
31}
32
33fn normalize_join(root: &Path, relative: &str) -> PathBuf {
36 let joined = root.join(relative);
37 let mut out = PathBuf::new();
38 for component in joined.components() {
39 match component {
40 Component::ParentDir => {
41 out.pop();
42 }
43 Component::CurDir => {}
44 c => out.push(c),
45 }
46 }
47 out
48}
49
50fn normalize_path(path: &Path) -> PathBuf {
52 let mut out = PathBuf::new();
53 for component in path.components() {
54 match component {
55 Component::ParentDir => {
56 out.pop();
57 }
58 Component::CurDir => {}
59 c => out.push(c),
60 }
61 }
62 out
63}
64
65pub(super) fn safe_join(root: &Path, relative: &str, label: &str) -> Result<PathBuf> {
71 let norm_root = normalize_path(root);
72 let path = normalize_join(root, relative);
73 if !path.starts_with(&norm_root) {
74 return Err(anyhow::anyhow!(
75 "Path traversal blocked: {} '{}' would escape the root directory",
76 label,
77 relative
78 ));
79 }
80 Ok(path)
81}
82
83pub(super) fn resolve_target(
84 target: &crate::addons::manifest::Target,
85 project_root: &Path,
86) -> Result<Vec<PathBuf>> {
87 use crate::addons::manifest::Target;
88 match target {
89 Target::File { file } => {
90 let path = safe_join(project_root, file, "target file")?;
91 Ok(vec![path])
92 }
93 Target::Glob { glob } => {
94 safe_join(project_root, glob, "glob pattern")?;
96 let pattern = project_root.join(glob).to_string_lossy().to_string();
97 let canonical_root = project_root
98 .canonicalize()
99 .with_context(|| format!("Cannot resolve project root '{}'", project_root.display()))?;
100 let paths = glob::glob(&pattern)?
101 .filter_map(|e| e.ok())
102 .filter(|p| {
104 p.canonicalize()
105 .map(|cp| cp.starts_with(&canonical_root))
106 .unwrap_or(false)
107 })
108 .collect();
109 Ok(paths)
110 }
111 }
112}