1use crate::syncdoc_debug;
2use ropey::Rope;
3use std::fs;
4use std::path::{Path, PathBuf};
5use textum::{Boundary, BoundaryMode, Snippet, Target};
6
7fn 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 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), }
35 };
36
37 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) }
50
51pub 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
60pub 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 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 if !source_dir.starts_with(&manifest_path) {
86 return Err("Source file is outside the manifest directory (security violation)".into());
87 }
88
89 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}