syncdoc_core/
config.rs

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