syncdoc_core/
config.rs

1use crate::path_utils::find_manifest_dir;
2use crate::syncdoc_debug;
3use ropey::Rope;
4use std::fs;
5use std::path::{Path, PathBuf};
6use textum::{Boundary, BoundaryMode, Snippet, Target};
7
8/// Get a specified attribute from the current crate's Cargo.toml, relative to the source file
9fn get_attribute_from_cargo_toml(
10    cargo_toml_path: &str,
11    attribute: &str,
12) -> Result<Option<String>, Box<dyn std::error::Error>> {
13    let content = fs::read_to_string(cargo_toml_path)?;
14    let rope = Rope::from_str(&content);
15
16    // Try to find the section text
17    let section_text = if let Ok(resolution) = (Snippet::Between {
18        start: Boundary::new(
19            Target::Literal("[package.metadata.syncdoc]".to_string()),
20            BoundaryMode::Exclude,
21        ),
22        end: Boundary::new(Target::Literal("[".to_string()), BoundaryMode::Exclude),
23    })
24    .resolve(&rope)
25    {
26        rope.slice(resolution.start..resolution.end).to_string()
27    } else {
28        let snippet = Snippet::From(Boundary::new(
29            Target::Literal("[package.metadata.syncdoc]".to_string()),
30            BoundaryMode::Exclude,
31        ));
32        match snippet.resolve(&rope) {
33            Ok(resolution) => rope.slice(resolution.start..resolution.end).to_string(),
34            Err(_) => return Ok(None), // No syncdoc section, return None
35        }
36    };
37
38    // Parse the specified attribute's value
39    for line in section_text.lines() {
40        let line = line.trim();
41        if line.starts_with(attribute) {
42            if let Some(value) = line.split('=').nth(1) {
43                let cleaned = value.trim().trim_matches('"').to_string();
44                return Ok(Some(cleaned));
45            }
46        }
47    }
48
49    Ok(None) // Attribute not found, return None
50}
51
52/// Get the cfg-attr from the current crate's Cargo.toml, relative to the source file
53pub fn get_cfg_attr(source_file: &str) -> Result<Option<String>, Box<dyn std::error::Error>> {
54    let source_path = Path::new(source_file);
55    let source_dir = source_path
56        .parent()
57        .ok_or("Source file has no parent directory")?;
58
59    let manifest_dir = find_manifest_dir(source_dir).ok_or("Could not find Cargo.toml")?;
60
61    let cargo_toml_path = manifest_dir.join("Cargo.toml");
62    get_attribute_from_cargo_toml(cargo_toml_path.to_str().unwrap(), "cfg-attr")
63}
64
65/// Get the docs-path from the current crate's Cargo.toml, relative to the source file
66pub fn get_docs_path(source_file: &str) -> Result<String, Box<dyn std::error::Error>> {
67    syncdoc_debug!("get_docs_path called:");
68    syncdoc_debug!("  source_file: {}", source_file);
69
70    let source_path = Path::new(source_file);
71    let source_dir = source_path
72        .parent()
73        .ok_or("Source file has no parent directory")?;
74
75    let manifest_dir = find_manifest_dir(source_dir).ok_or("Could not find Cargo.toml")?;
76    syncdoc_debug!("  manifest_dir: {}", manifest_dir.display());
77
78    let cargo_toml_path = manifest_dir.join("Cargo.toml");
79    let docs_path = get_attribute_from_cargo_toml(cargo_toml_path.to_str().unwrap(), "docs-path")?
80        .ok_or("docs-path not found")?;
81    syncdoc_debug!("  docs_path from toml: {}", docs_path);
82
83    let manifest_path = manifest_dir.canonicalize()?;
84    syncdoc_debug!("  manifest_path (canonical): {}", manifest_path.display());
85
86    let source_dir_canonical = source_dir.canonicalize()?;
87    syncdoc_debug!(
88        "  source_dir (canonical): {}",
89        source_dir_canonical.display()
90    );
91
92    // Security check: ensure source_dir is within manifest_dir
93    if !source_dir_canonical.starts_with(&manifest_path) {
94        return Err("Source file is outside the manifest directory (security violation)".into());
95    }
96
97    // Calculate number of ".." needed to go from source_dir to manifest_dir
98    let relative_path = source_dir_canonical
99        .strip_prefix(&manifest_path)
100        .map_err(|_| "Failed to strip prefix")?;
101    syncdoc_debug!("  relative_path (stripped): {}", relative_path.display());
102
103    let depth = relative_path.components().count();
104    syncdoc_debug!("  depth: {}", depth);
105
106    let mut result = PathBuf::new();
107
108    for _ in 0..depth {
109        result.push("..");
110    }
111
112    result.push(&docs_path);
113    let result_str = result.to_string_lossy().to_string();
114    syncdoc_debug!("  final result: {}", result_str);
115    Ok(result_str)
116}
117
118#[cfg(test)]
119mod docs_path_tests {
120    use super::*;
121    use std::io::Write;
122    use tempfile::NamedTempFile;
123
124    fn get_docs_path_from_file(
125        cargo_toml_path: &str,
126    ) -> Result<String, Box<dyn std::error::Error>> {
127        let docs_path = get_attribute_from_cargo_toml(cargo_toml_path, "docs-path")?
128            .ok_or("docs-path not found")?;
129        Ok(docs_path)
130    }
131
132    #[test]
133    fn test_docs_path_with_following_section() {
134        let content = r#"
135[package]
136name = "myproject"
137
138[package.metadata.syncdoc]
139docs-path = "docs"
140
141[dependencies]
142serde = "1.0"
143"#;
144        let mut temp = NamedTempFile::new().unwrap();
145        write!(temp, "{}", content).unwrap();
146        temp.flush().unwrap();
147
148        let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
149        assert_eq!(result, "docs");
150    }
151
152    #[test]
153    fn test_docs_path_at_eof() {
154        let content = r#"
155[package]
156name = "myproject"
157
158[package.metadata.syncdoc]
159docs-path = "documentation"
160"#;
161        let mut temp = NamedTempFile::new().unwrap();
162        write!(temp, "{}", content).unwrap();
163        temp.flush().unwrap();
164
165        let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
166        assert_eq!(result, "documentation");
167    }
168
169    #[test]
170    fn test_docs_path_with_extra_whitespace() {
171        let content = r#"
172[package.metadata.syncdoc]
173  docs-path  =  "my-docs"
174"#;
175        let mut temp = NamedTempFile::new().unwrap();
176        write!(temp, "{}", content).unwrap();
177        temp.flush().unwrap();
178
179        let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
180        assert_eq!(result, "my-docs");
181    }
182
183    #[test]
184    fn test_docs_path_without_quotes() {
185        let content = r#"
186[package.metadata.syncdoc]
187docs-path = docs
188"#;
189        let mut temp = NamedTempFile::new().unwrap();
190        write!(temp, "{}", content).unwrap();
191        temp.flush().unwrap();
192
193        let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
194        assert_eq!(result, "docs");
195    }
196
197    #[test]
198    fn test_missing_syncdoc_section() {
199        let content = r#"
200[package]
201name = "myproject"
202"#;
203        let mut temp = NamedTempFile::new().unwrap();
204        write!(temp, "{}", content).unwrap();
205        temp.flush().unwrap();
206
207        let result = get_docs_path_from_file(temp.path().to_str().unwrap());
208        assert!(result.is_err());
209    }
210
211    #[test]
212    fn test_missing_docs_path_field() {
213        let content = r#"
214[package.metadata.syncdoc]
215other-field = "value"
216"#;
217        let mut temp = NamedTempFile::new().unwrap();
218        write!(temp, "{}", content).unwrap();
219        temp.flush().unwrap();
220
221        let result = get_docs_path_from_file(temp.path().to_str().unwrap());
222        assert!(result.is_err());
223        assert!(result
224            .unwrap_err()
225            .to_string()
226            .contains("docs-path not found"));
227    }
228
229    #[test]
230    fn test_docs_path_with_multiple_fields() {
231        let content = r#"
232[package.metadata.syncdoc]
233enable = true
234docs-path = "api-docs"
235output-format = "markdown"
236
237[dependencies]
238"#;
239        let mut temp = NamedTempFile::new().unwrap();
240        write!(temp, "{}", content).unwrap();
241        temp.flush().unwrap();
242
243        let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
244        assert_eq!(result, "api-docs");
245    }
246}
247
248#[cfg(test)]
249mod cfg_attr_tests {
250    use super::*;
251    use std::io::Write;
252    use tempfile::NamedTempFile;
253
254    fn get_cfg_attr_from_file(cargo_toml_path: &str) -> Result<String, Box<dyn std::error::Error>> {
255        let cfg_attr = get_attribute_from_cargo_toml(cargo_toml_path, "cfg-attr")?
256            .ok_or("cfg-attr not found")?;
257        Ok(cfg_attr)
258    }
259
260    #[test]
261    fn test_cfg_attr_not_set() {
262        let content = r#"
263[package]
264name = "myproject"
265
266[package.metadata.syncdoc]
267"#;
268        let mut temp = NamedTempFile::new().unwrap();
269        write!(temp, "{}", content).unwrap();
270        temp.flush().unwrap();
271
272        let result = get_cfg_attr_from_file(temp.path().to_str().unwrap());
273        assert!(result.is_err());
274        assert!(result
275            .unwrap_err()
276            .to_string()
277            .contains("cfg-attr not found"));
278    }
279
280    #[test]
281    fn test_cfg_attr_set_as_doc() {
282        let content = r#"
283[package]
284name = "myproject"
285
286[package.metadata.syncdoc]
287cfg-attr = "doc"
288"#;
289        let mut temp = NamedTempFile::new().unwrap();
290        write!(temp, "{}", content).unwrap();
291        temp.flush().unwrap();
292
293        let result = get_cfg_attr_from_file(temp.path().to_str().unwrap()).unwrap();
294        assert_eq!(result, "doc");
295    }
296
297    #[test]
298    fn test_cfg_attr_set_as_custom() {
299        let content = r#"
300[package]
301name = "myproject"
302
303[package.metadata.syncdoc]
304cfg-attr = "a-custom-attr"
305"#;
306        let mut temp = NamedTempFile::new().unwrap();
307        write!(temp, "{}", content).unwrap();
308        temp.flush().unwrap();
309
310        let result = get_cfg_attr_from_file(temp.path().to_str().unwrap()).unwrap();
311        assert_eq!(result, "a-custom-attr");
312    }
313}