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
52fn resolve_source_path(source_file: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
54 let source_path = Path::new(source_file);
55
56 let source_path = if source_path.is_absolute() {
58 source_path.to_path_buf()
59 } else {
60 std::env::current_dir()?.join(source_path)
61 };
62
63 Ok(source_path)
64}
65
66pub fn get_cfg_attr(source_file: &str) -> Result<Option<String>, Box<dyn std::error::Error>> {
68 let source_path = resolve_source_path(source_file)?;
69 let source_dir = source_path
70 .parent()
71 .ok_or("Source file has no parent directory")?;
72
73 let manifest_dir = find_manifest_dir(source_dir).ok_or("Could not find Cargo.toml")?;
74
75 let cargo_toml_path = manifest_dir.join("Cargo.toml");
76 get_attribute_from_cargo_toml(cargo_toml_path.to_str().unwrap(), "cfg-attr")
77}
78
79pub fn get_docs_path(source_file: &str) -> Result<String, Box<dyn std::error::Error>> {
81 syncdoc_debug!("get_docs_path called:");
82 syncdoc_debug!(" source_file: {}", source_file);
83
84 let source_path = resolve_source_path(source_file)?;
85
86 let source_dir = source_path
87 .parent()
88 .ok_or("Source file has no parent directory")?;
89
90 let manifest_dir = find_manifest_dir(source_dir).ok_or("Could not find Cargo.toml")?;
91 syncdoc_debug!(" manifest_dir: {}", manifest_dir.display());
92
93 let cargo_toml_path = manifest_dir.join("Cargo.toml");
94 let docs_path = get_attribute_from_cargo_toml(cargo_toml_path.to_str().unwrap(), "docs-path")?
95 .ok_or("docs-path not found")?;
96 syncdoc_debug!(" docs_path from toml: {}", docs_path);
97
98 let manifest_path = manifest_dir.canonicalize()?;
99 syncdoc_debug!(" manifest_path (canonical): {}", manifest_path.display());
100
101 let source_dir_canonical = source_dir.canonicalize()?;
102 syncdoc_debug!(
103 " source_dir (canonical): {}",
104 source_dir_canonical.display()
105 );
106
107 if !source_dir_canonical.starts_with(&manifest_path) {
109 return Err("Source file is outside the manifest directory (security violation)".into());
110 }
111
112 let relative_path = source_dir_canonical
114 .strip_prefix(&manifest_path)
115 .map_err(|_| "Failed to strip prefix")?;
116 syncdoc_debug!(" relative_path (stripped): {}", relative_path.display());
117
118 let depth = relative_path.components().count();
119 syncdoc_debug!(" depth: {}", depth);
120
121 let mut result = PathBuf::new();
122
123 for _ in 0..depth {
124 result.push("..");
125 }
126
127 result.push(&docs_path);
128 let result_str = result.to_string_lossy().to_string();
129 syncdoc_debug!(" final result: {}", result_str);
130 Ok(result_str)
131}
132
133#[cfg(test)]
134mod docs_path_tests {
135 use super::*;
136 use std::io::Write;
137 use tempfile::NamedTempFile;
138
139 fn get_docs_path_from_file(
140 cargo_toml_path: &str,
141 ) -> Result<String, Box<dyn std::error::Error>> {
142 let docs_path = get_attribute_from_cargo_toml(cargo_toml_path, "docs-path")?
143 .ok_or("docs-path not found")?;
144 Ok(docs_path)
145 }
146
147 #[test]
148 fn test_docs_path_with_following_section() {
149 let content = r#"
150[package]
151name = "myproject"
152
153[package.metadata.syncdoc]
154docs-path = "docs"
155
156[dependencies]
157serde = "1.0"
158"#;
159 let mut temp = NamedTempFile::new().unwrap();
160 write!(temp, "{}", content).unwrap();
161 temp.flush().unwrap();
162
163 let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
164 assert_eq!(result, "docs");
165 }
166
167 #[test]
168 fn test_docs_path_at_eof() {
169 let content = r#"
170[package]
171name = "myproject"
172
173[package.metadata.syncdoc]
174docs-path = "documentation"
175"#;
176 let mut temp = NamedTempFile::new().unwrap();
177 write!(temp, "{}", content).unwrap();
178 temp.flush().unwrap();
179
180 let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
181 assert_eq!(result, "documentation");
182 }
183
184 #[test]
185 fn test_docs_path_with_extra_whitespace() {
186 let content = r#"
187[package.metadata.syncdoc]
188 docs-path = "my-docs"
189"#;
190 let mut temp = NamedTempFile::new().unwrap();
191 write!(temp, "{}", content).unwrap();
192 temp.flush().unwrap();
193
194 let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
195 assert_eq!(result, "my-docs");
196 }
197
198 #[test]
199 fn test_docs_path_without_quotes() {
200 let content = r#"
201[package.metadata.syncdoc]
202docs-path = docs
203"#;
204 let mut temp = NamedTempFile::new().unwrap();
205 write!(temp, "{}", content).unwrap();
206 temp.flush().unwrap();
207
208 let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
209 assert_eq!(result, "docs");
210 }
211
212 #[test]
213 fn test_missing_syncdoc_section() {
214 let content = r#"
215[package]
216name = "myproject"
217"#;
218 let mut temp = NamedTempFile::new().unwrap();
219 write!(temp, "{}", content).unwrap();
220 temp.flush().unwrap();
221
222 let result = get_docs_path_from_file(temp.path().to_str().unwrap());
223 assert!(result.is_err());
224 }
225
226 #[test]
227 fn test_missing_docs_path_field() {
228 let content = r#"
229[package.metadata.syncdoc]
230other-field = "value"
231"#;
232 let mut temp = NamedTempFile::new().unwrap();
233 write!(temp, "{}", content).unwrap();
234 temp.flush().unwrap();
235
236 let result = get_docs_path_from_file(temp.path().to_str().unwrap());
237 assert!(result.is_err());
238 assert!(result
239 .unwrap_err()
240 .to_string()
241 .contains("docs-path not found"));
242 }
243
244 #[test]
245 fn test_docs_path_with_multiple_fields() {
246 let content = r#"
247[package.metadata.syncdoc]
248enable = true
249docs-path = "api-docs"
250output-format = "markdown"
251
252[dependencies]
253"#;
254 let mut temp = NamedTempFile::new().unwrap();
255 write!(temp, "{}", content).unwrap();
256 temp.flush().unwrap();
257
258 let result = get_docs_path_from_file(temp.path().to_str().unwrap()).unwrap();
259 assert_eq!(result, "api-docs");
260 }
261}
262
263#[cfg(test)]
264mod relative_path_tests {
265 use super::*;
266 use std::fs;
267 use std::io::Write;
268 use tempfile::TempDir;
269
270 #[test]
271 fn test_get_docs_path_with_relative_source_file() {
272 let temp_dir = TempDir::new().unwrap();
274 let project_root = temp_dir.path();
275
276 let cargo_toml_path = project_root.join("Cargo.toml");
278 let mut cargo_toml = fs::File::create(&cargo_toml_path).unwrap();
279 write!(
280 cargo_toml,
281 r#"
282[package]
283name = "test-project"
284
285[package.metadata.syncdoc]
286docs-path = "docs"
287"#
288 )
289 .unwrap();
290 cargo_toml.flush().unwrap();
291
292 let src_dir = project_root.join("src");
294 fs::create_dir(&src_dir).unwrap();
295
296 let lib_rs = src_dir.join("lib.rs");
298 fs::File::create(&lib_rs).unwrap();
299
300 let original_dir = std::env::current_dir().unwrap();
302 std::env::set_current_dir(project_root).unwrap();
303
304 let result = get_docs_path("src/lib.rs");
306
307 std::env::set_current_dir(original_dir).unwrap();
309
310 assert!(
312 result.is_ok(),
313 "Should handle relative source file paths. Error: {:?}",
314 result.err()
315 );
316
317 let docs_path = result.unwrap();
318 assert_eq!(docs_path, "../docs");
319 }
320
321 #[test]
322 fn test_get_docs_path_with_nested_relative_source_file() {
323 let temp_dir = TempDir::new().unwrap();
324 let project_root = temp_dir.path();
325
326 let cargo_toml_path = project_root.join("Cargo.toml");
327 let mut cargo_toml = fs::File::create(&cargo_toml_path).unwrap();
328 write!(
329 cargo_toml,
330 r#"
331[package]
332name = "test-project"
333
334[package.metadata.syncdoc]
335docs-path = "documentation"
336"#
337 )
338 .unwrap();
339 cargo_toml.flush().unwrap();
340
341 let src_dir = project_root.join("src");
342 fs::create_dir(&src_dir).unwrap();
343
344 let nested_dir = src_dir.join("nested");
345 fs::create_dir(&nested_dir).unwrap();
346
347 let nested_file = nested_dir.join("module.rs");
348 fs::File::create(&nested_file).unwrap();
349
350 let original_dir = std::env::current_dir().unwrap();
351 std::env::set_current_dir(project_root).unwrap();
352
353 let result = get_docs_path("src/nested/module.rs");
355
356 std::env::set_current_dir(original_dir).unwrap();
357
358 assert!(result.is_ok(), "Should handle nested relative paths");
359 let docs_path = result.unwrap();
360 assert_eq!(docs_path, "../../documentation");
361 }
362}
363
364#[cfg(test)]
365mod cfg_attr_tests {
366 use super::*;
367 use std::io::Write;
368 use tempfile::NamedTempFile;
369
370 fn get_cfg_attr_from_file(cargo_toml_path: &str) -> Result<String, Box<dyn std::error::Error>> {
371 let cfg_attr = get_attribute_from_cargo_toml(cargo_toml_path, "cfg-attr")?
372 .ok_or("cfg-attr not found")?;
373 Ok(cfg_attr)
374 }
375
376 #[test]
377 fn test_cfg_attr_not_set() {
378 let content = r#"
379[package]
380name = "myproject"
381
382[package.metadata.syncdoc]
383"#;
384 let mut temp = NamedTempFile::new().unwrap();
385 write!(temp, "{}", content).unwrap();
386 temp.flush().unwrap();
387
388 let result = get_cfg_attr_from_file(temp.path().to_str().unwrap());
389 assert!(result.is_err());
390 assert!(result
391 .unwrap_err()
392 .to_string()
393 .contains("cfg-attr not found"));
394 }
395
396 #[test]
397 fn test_cfg_attr_set_as_doc() {
398 let content = r#"
399[package]
400name = "myproject"
401
402[package.metadata.syncdoc]
403cfg-attr = "doc"
404"#;
405 let mut temp = NamedTempFile::new().unwrap();
406 write!(temp, "{}", content).unwrap();
407 temp.flush().unwrap();
408
409 let result = get_cfg_attr_from_file(temp.path().to_str().unwrap()).unwrap();
410 assert_eq!(result, "doc");
411 }
412
413 #[test]
414 fn test_cfg_attr_set_as_custom() {
415 let content = r#"
416[package]
417name = "myproject"
418
419[package.metadata.syncdoc]
420cfg-attr = "a-custom-attr"
421"#;
422 let mut temp = NamedTempFile::new().unwrap();
423 write!(temp, "{}", content).unwrap();
424 temp.flush().unwrap();
425
426 let result = get_cfg_attr_from_file(temp.path().to_str().unwrap()).unwrap();
427 assert_eq!(result, "a-custom-attr");
428 }
429}