oxide_cli/templates/
generator.rs1use std::{
2 fs,
3 path::{Component, Path, PathBuf},
4};
5
6use anyhow::{Result, anyhow};
7use tera::{Context, Tera};
8
9use crate::templates::TemplateFile;
10
11pub fn extract_template(files: &[TemplateFile], project_name: &str) -> Result<()> {
12 let output_path = PathBuf::from(project_name);
13 fs::create_dir_all(&output_path)?;
14
15 let mut context = Context::new();
16 context.insert("project_name", project_name);
17 context.insert("project_name_kebab", &to_kebab_case(project_name));
18 context.insert("project_name_snake", &to_snake_case(project_name));
19
20 let mut tera = Tera::default();
21
22 extract_dir_contents(files, &output_path, &mut tera, &context)?;
23
24 Ok(())
25}
26
27pub fn to_kebab_case(s: &str) -> String {
28 s.chars()
29 .map(|c| match c {
30 '_' | ' ' => '-',
31 _ => c,
32 })
33 .collect::<String>()
34 .to_lowercase()
35}
36
37pub fn to_snake_case(s: &str) -> String {
38 s.chars()
39 .map(|c| match c {
40 '-' | ' ' => '_',
41 _ => c,
42 })
43 .collect::<String>()
44 .to_lowercase()
45}
46
47fn safe_template_path(base: &Path, relative: &Path) -> Result<PathBuf> {
51 let joined = base.join(relative);
52 let mut out = PathBuf::new();
53 for component in joined.components() {
54 match component {
55 Component::ParentDir => {
56 out.pop();
57 }
58 Component::CurDir => {}
59 c => out.push(c),
60 }
61 }
62 let mut norm_base = PathBuf::new();
64 for component in base.components() {
65 match component {
66 Component::ParentDir => {
67 norm_base.pop();
68 }
69 Component::CurDir => {}
70 c => norm_base.push(c),
71 }
72 }
73 if !out.starts_with(&norm_base) {
74 return Err(anyhow!(
75 "Path traversal blocked: template file '{}' would escape the output directory",
76 relative.display()
77 ));
78 }
79 Ok(out)
80}
81
82pub fn extract_dir_contents(
83 files: &[TemplateFile],
84 base_path: &Path,
85 tera: &mut Tera,
86 context: &Context,
87) -> Result<()> {
88 for file in files {
89 let file_name = file
90 .path
91 .file_name()
92 .ok_or_else(|| anyhow::anyhow!("Invalid file path: {}", file.path.display()))?;
93 let file_name_str = file_name.to_string_lossy();
94 let template_key = file.path.to_string_lossy();
95
96 let output_path = safe_template_path(base_path, &file.path)?;
97 if let Some(parent) = output_path.parent() {
98 fs::create_dir_all(parent)?;
99 }
100
101 if file_name_str.ends_with(".tera") {
102 let output_name = file_name_str.trim_end_matches(".tera");
103 let output_path = output_path.with_file_name(output_name);
104
105 let template_content = std::str::from_utf8(&file.contents)?;
106 tera.add_raw_template(&template_key, template_content)?;
107 let rendered = tera.render(&template_key, context)?;
108
109 fs::write(&output_path, rendered)?;
110 println!(" ✓ {}", output_path.display());
111 } else {
112 fs::write(&output_path, &file.contents)?;
113 println!(" ✓ {}", output_path.display());
114 }
115 }
116 Ok(())
117}