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
8fn 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 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), }
36 };
37
38 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) }
51
52pub 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
65pub 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 if !source_dir_canonical.starts_with(&manifest_path) {
94 return Err("Source file is outside the manifest directory (security violation)".into());
95 }
96
97 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}