1use crate::error::Error;
2
3use crate::constants::*;
4use fehler::throws;
5use serde_json::Value;
6use std::path::Path;
7use std::path::PathBuf;
8use tokio::fs;
9
10#[macro_export]
11macro_rules! construct_path {
12 ($root:expr, $($component:expr),*) => {
13 {
14 let mut path = $root.to_owned();
15 $(path = path.join($component);)*
16 path
17 }
18 };
19}
20#[macro_export]
21macro_rules! load_template {
22 ($file:expr) => {
23 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), $file))
24 };
25}
26
27#[throws]
28pub async fn create_directory_all(path: &PathBuf) {
29 match path.exists() {
30 true => {}
31 false => {
32 fs::create_dir_all(path).await?;
33 }
34 };
35}
36
37#[throws]
38pub async fn create_file(root: &PathBuf, path: &PathBuf, content: &str) {
39 let file = path.strip_prefix(root)?.to_str().unwrap_or_default();
40
41 match path.exists() {
42 true => {
43 println!("{SKIP} [{file}] already exists")
44 }
45 false => {
46 fs::write(path, content).await?;
47 println!("{FINISH} [{file}] created");
48 }
49 };
50}
51
52#[throws]
53pub fn get_fuzz_id(fuzz_dir_path: &Path) -> i32 {
54 if fuzz_dir_path.exists() {
55 if fuzz_dir_path.read_dir()?.next().is_none() {
56 0
57 } else {
58 let entries = fuzz_dir_path.read_dir()?;
59 let mut max_num = -1;
60 for entry in entries {
61 let entry = entry?;
62 let file_name = entry.file_name().into_string().unwrap_or_default();
63 if file_name.starts_with("fuzz_") {
64 let stripped = file_name.strip_prefix("fuzz_").unwrap_or_default();
65 let num = stripped.parse::<i32>()?;
66 max_num = max_num.max(num);
67 }
68 }
69 max_num + 1
70 }
71 } else {
72 0
73 }
74}
75
76#[throws]
78pub async fn ensure_fuzz_artifacts_dir() -> PathBuf {
79 let artifacts_dir = PathBuf::from(".fuzz-artifacts");
80 create_directory_all(&artifacts_dir).await?;
81 artifacts_dir
82}
83
84#[throws]
87pub async fn generate_unique_fuzz_filename(
88 base_name: &str,
89 fuzz_test_name: &str,
90 extension: &str,
91) -> PathBuf {
92 let artifacts_dir = ensure_fuzz_artifacts_dir().await?;
93 let base_filename = format!("{}_{}.{}", base_name, fuzz_test_name, extension);
94 let mut target_path = artifacts_dir.join(&base_filename);
95
96 if target_path.exists() {
98 use chrono::DateTime;
99 use chrono::Local;
100
101 let now: DateTime<Local> = Local::now();
103
104 let timestamp = now.format("%Y-%m-%d_%H-%M-%S").to_string();
106 let unique_filename = format!(
107 "{}_{}-{}.{}",
108 base_name, fuzz_test_name, timestamp, extension
109 );
110 target_path = artifacts_dir.join(&unique_filename);
111
112 if target_path.exists() {
114 let timestamp_with_ms = now.format("%Y-%m-%d_%H-%M-%S-%3f").to_string();
115 let unique_filename = format!(
116 "{}_{}-{}.{}",
117 base_name, fuzz_test_name, timestamp_with_ms, extension
118 );
119 target_path = artifacts_dir.join(&unique_filename);
120 }
121 }
122
123 target_path
124}
125
126fn merge_json(existing: &mut Value, new: &Value) {
128 match (existing, new) {
129 (Value::Object(existing_map), Value::Object(new_map)) => {
130 for (key, new_val) in new_map {
131 existing_map
132 .entry(key.clone())
133 .and_modify(|existing_val| merge_json(existing_val, new_val))
134 .or_insert_with(|| new_val.clone());
135 }
136 }
137 (Value::Array(existing_arr), Value::Array(new_arr)) => {
138 for item in new_arr {
140 if !existing_arr.contains(item) {
141 existing_arr.push(item.clone());
142 }
143 }
144 }
145 (existing_val, new_val) => {
146 *existing_val = new_val.clone();
147 }
148 }
149}
150
151fn strip_trailing_commas(json_str: &str) -> String {
153 let chars: Vec<char> = json_str.chars().collect();
154 let mut result = String::with_capacity(json_str.len());
155
156 for i in 0..chars.len() {
157 if chars[i] == ',' {
158 let remaining = &chars[i + 1..];
160 if remaining.iter().take_while(|c| c.is_whitespace()).count() == remaining.len()
161 || remaining
162 .iter()
163 .find(|c| !c.is_whitespace())
164 .is_some_and(|c| *c == '}' || *c == ']')
165 {
166 continue;
167 }
168 }
169 result.push(chars[i]);
170 }
171 result
172}
173
174#[throws]
176pub async fn create_or_update_json_file(root: &PathBuf, path: &PathBuf, content: &str) {
177 let file = path.strip_prefix(root)?.to_str().unwrap_or_default();
178
179 if !path.exists() {
180 fs::write(path, content).await?;
181 println!("{FINISH} [{file}] created");
182 return;
183 }
184
185 let existing_content = fs::read_to_string(path).await?;
186
187 if existing_content.trim().is_empty() {
189 fs::write(path, content).await?;
190 println!("{FINISH} [{file}] created (was empty)");
191 return;
192 }
193
194 let cleaned = strip_trailing_commas(&existing_content);
196 let mut existing_json: Value = match serde_json::from_str(&cleaned) {
197 Ok(json) => json,
198 Err(e) => {
199 eprintln!("Warning: Invalid JSON in {}: {}", file, e);
201 let backup_path = path.with_extension("json.backup");
202 fs::write(&backup_path, &existing_content).await?;
203 fs::write(path, content).await?;
204 println!("{UPDATED} [{file}] (backed up invalid JSON)");
205 return;
206 }
207 };
208
209 let new_json: Value = serde_json::from_str(content)?;
211 merge_json(&mut existing_json, &new_json);
212 let merged = serde_json::to_string_pretty(&existing_json)?;
213 fs::write(path, merged).await?;
214 println!("{UPDATED} [{file}] merged with existing settings");
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use serde_json::json;
221
222 #[test]
223 fn test_merge_json_objects() {
224 let mut existing = json!({
225 "key1": "value1",
226 "key2": {
227 "nested": "old"
228 }
229 });
230
231 let new = json!({
232 "key2": {
233 "nested": "new",
234 "added": "value"
235 },
236 "key3": "value3"
237 });
238
239 merge_json(&mut existing, &new);
240
241 assert_eq!(existing["key1"], "value1");
242 assert_eq!(existing["key2"]["nested"], "new");
243 assert_eq!(existing["key2"]["added"], "value");
244 assert_eq!(existing["key3"], "value3");
245 }
246
247 #[test]
248 fn test_merge_json_arrays() {
249 let mut existing = json!({
250 "linkedProjects": ["./Cargo.toml"]
251 });
252
253 let new = json!({
254 "linkedProjects": ["./trident-tests/Cargo.toml"]
255 });
256
257 merge_json(&mut existing, &new);
258
259 let projects = existing["linkedProjects"].as_array().unwrap();
260 assert_eq!(projects.len(), 2);
261 assert!(projects.contains(&json!("./Cargo.toml")));
262 assert!(projects.contains(&json!("./trident-tests/Cargo.toml")));
263 }
264
265 #[test]
266 fn test_merge_json_arrays_no_duplicates() {
267 let mut existing = json!({
268 "linkedProjects": ["./Cargo.toml", "./trident-tests/Cargo.toml"]
269 });
270
271 let new = json!({
272 "linkedProjects": ["./Cargo.toml", "./trident-tests/Cargo.toml"]
273 });
274
275 merge_json(&mut existing, &new);
276
277 let projects = existing["linkedProjects"].as_array().unwrap();
278 assert_eq!(projects.len(), 2);
279 }
280
281 #[test]
282 fn test_merge_json_primitive_override() {
283 let mut existing = json!({
284 "setting": "old_value"
285 });
286
287 let new = json!({
288 "setting": "new_value"
289 });
290
291 merge_json(&mut existing, &new);
292
293 assert_eq!(existing["setting"], "new_value");
294 }
295
296 #[test]
297 fn test_merge_json_complex() {
298 let mut existing = json!({
299 "rust-analyzer.linkedProjects": ["./Cargo.toml"],
300 "editor.formatOnSave": true,
301 "custom": {
302 "nested": "value"
303 }
304 });
305
306 let new = json!({
307 "rust-analyzer.linkedProjects": ["./trident-tests/Cargo.toml"],
308 "editor.rulers": [80, 120]
309 });
310
311 merge_json(&mut existing, &new);
312
313 let projects = existing["rust-analyzer.linkedProjects"].as_array().unwrap();
314 assert_eq!(projects.len(), 2);
315 assert_eq!(existing["editor.formatOnSave"], true);
316 assert_eq!(existing["editor.rulers"], json!([80, 120]));
317 assert_eq!(existing["custom"]["nested"], "value");
318 }
319
320 #[test]
321 fn test_strip_trailing_commas_simple() {
322 let input = r#"{
323 "key": "value",
324}"#;
325 let expected = r#"{
326 "key": "value"
327}"#;
328 assert_eq!(strip_trailing_commas(input), expected);
329 }
330
331 #[test]
332 fn test_strip_trailing_commas_array() {
333 let input = r#"{
334 "items": [
335 "item1",
336 "item2",
337 ]
338}"#;
339 let expected = r#"{
340 "items": [
341 "item1",
342 "item2"
343 ]
344}"#;
345 assert_eq!(strip_trailing_commas(input), expected);
346 }
347
348 #[test]
349 fn test_strip_trailing_commas_nested() {
350 let input = r#"{
351 "outer": {
352 "inner": "value",
353 },
354 "array": [1, 2, 3,],
355}"#;
356 let expected = r#"{
357 "outer": {
358 "inner": "value"
359 },
360 "array": [1, 2, 3]
361}"#;
362 assert_eq!(strip_trailing_commas(input), expected);
363 }
364
365 #[test]
366 fn test_strip_trailing_commas_preserves_valid_commas() {
367 let input = r#"{
368 "key1": "value1",
369 "key2": "value2"
370}"#;
371 assert_eq!(strip_trailing_commas(input), input);
373 }
374
375 #[test]
376 fn test_strip_trailing_commas_vscode_settings() {
377 let input = r#"{
378 "rust-analyzer.linkedProjects": [
379 "./Cargo.toml",
380 ],
381 "editor.formatOnSave": true,
382}"#;
383 let cleaned = strip_trailing_commas(input);
384 let result: Result<Value, _> = serde_json::from_str(&cleaned);
386 assert!(result.is_ok());
387
388 let json = result.unwrap();
389 assert_eq!(json["editor.formatOnSave"], true);
390 let projects = json["rust-analyzer.linkedProjects"].as_array().unwrap();
391 assert_eq!(projects.len(), 1);
392 }
393
394 #[test]
395 fn test_merge_linked_projects_when_cargo_toml_exists() {
396 let mut existing = json!({
398 "rust-analyzer.linkedProjects": ["./Cargo.toml"],
399 "editor.formatOnSave": true
400 });
401
402 let new = json!({
403 "rust-analyzer.linkedProjects": [
404 "./Cargo.toml",
405 "./trident-tests/Cargo.toml"
406 ]
407 });
408
409 merge_json(&mut existing, &new);
410
411 let projects = existing["rust-analyzer.linkedProjects"].as_array().unwrap();
412 assert_eq!(projects.len(), 2);
413 assert_eq!(projects[0], "./Cargo.toml");
414 assert_eq!(projects[1], "./trident-tests/Cargo.toml");
415 assert_eq!(existing["editor.formatOnSave"], true);
416 }
417
418 #[test]
419 fn test_merge_linked_projects_when_cargo_toml_missing() {
420 let mut existing = json!({
422 "rust-analyzer.linkedProjects": [],
423 "editor.formatOnSave": true
424 });
425
426 let new = json!({
427 "rust-analyzer.linkedProjects": [
428 "./Cargo.toml",
429 "./trident-tests/Cargo.toml"
430 ]
431 });
432
433 merge_json(&mut existing, &new);
434
435 let projects = existing["rust-analyzer.linkedProjects"].as_array().unwrap();
436 assert_eq!(projects.len(), 2);
437 assert!(projects.contains(&json!("./Cargo.toml")));
438 assert!(projects.contains(&json!("./trident-tests/Cargo.toml")));
439 assert_eq!(existing["editor.formatOnSave"], true);
440 }
441
442 #[test]
443 fn test_merge_linked_projects_when_both_exist() {
444 let mut existing = json!({
446 "rust-analyzer.linkedProjects": [
447 "./Cargo.toml",
448 "./trident-tests/Cargo.toml"
449 ]
450 });
451
452 let new = json!({
453 "rust-analyzer.linkedProjects": [
454 "./Cargo.toml",
455 "./trident-tests/Cargo.toml"
456 ]
457 });
458
459 merge_json(&mut existing, &new);
460
461 let projects = existing["rust-analyzer.linkedProjects"].as_array().unwrap();
462 assert_eq!(projects.len(), 2);
463 }
464
465 #[test]
466 fn test_merge_linked_projects_with_other_paths() {
467 let mut existing = json!({
469 "rust-analyzer.linkedProjects": [
470 "./Cargo.toml",
471 "./other-project/Cargo.toml"
472 ]
473 });
474
475 let new = json!({
476 "rust-analyzer.linkedProjects": [
477 "./Cargo.toml",
478 "./trident-tests/Cargo.toml"
479 ]
480 });
481
482 merge_json(&mut existing, &new);
483
484 let projects = existing["rust-analyzer.linkedProjects"].as_array().unwrap();
485 assert_eq!(projects.len(), 3);
486 assert!(projects.contains(&json!("./Cargo.toml")));
487 assert!(projects.contains(&json!("./other-project/Cargo.toml")));
488 assert!(projects.contains(&json!("./trident-tests/Cargo.toml")));
489 }
490
491 #[test]
492 fn test_merge_creates_linked_projects_when_missing() {
493 let mut existing = json!({
495 "editor.formatOnSave": true
496 });
497
498 let new = json!({
499 "rust-analyzer.linkedProjects": [
500 "./Cargo.toml",
501 "./trident-tests/Cargo.toml"
502 ]
503 });
504
505 merge_json(&mut existing, &new);
506
507 assert!(existing.get("rust-analyzer.linkedProjects").is_some());
508 let projects = existing["rust-analyzer.linkedProjects"].as_array().unwrap();
509 assert_eq!(projects.len(), 2);
510 assert!(projects.contains(&json!("./Cargo.toml")));
511 assert!(projects.contains(&json!("./trident-tests/Cargo.toml")));
512 assert_eq!(existing["editor.formatOnSave"], true);
513 }
514
515 #[tokio::test]
516 async fn test_create_or_update_json_file_empty_file() {
517 use tempfile::TempDir;
518
519 let temp_dir = TempDir::new().unwrap();
520 let root = temp_dir.path().to_path_buf();
521 let vscode_dir = root.join(".vscode");
522 std::fs::create_dir_all(&vscode_dir).unwrap();
523
524 let settings_path = vscode_dir.join("settings.json");
525
526 std::fs::write(&settings_path, "").unwrap();
528
529 let new_content = r#"{
530 "rust-analyzer.linkedProjects": [
531 "./Cargo.toml",
532 "./trident-tests/Cargo.toml"
533 ]
534}"#;
535
536 create_or_update_json_file(&root, &settings_path, new_content)
538 .await
539 .unwrap();
540
541 let backup_path = settings_path.with_extension("json.backup");
543 assert!(!backup_path.exists());
544
545 let content = std::fs::read_to_string(&settings_path).unwrap();
547 let json: Value = serde_json::from_str(&content).unwrap();
548 assert!(json.get("rust-analyzer.linkedProjects").is_some());
549 }
550
551 #[tokio::test]
552 async fn test_create_or_update_json_file_whitespace_only() {
553 use tempfile::TempDir;
554
555 let temp_dir = TempDir::new().unwrap();
556 let root = temp_dir.path().to_path_buf();
557 let vscode_dir = root.join(".vscode");
558 std::fs::create_dir_all(&vscode_dir).unwrap();
559
560 let settings_path = vscode_dir.join("settings.json");
561
562 std::fs::write(&settings_path, " \n\t \n ").unwrap();
564
565 let new_content = r#"{
566 "rust-analyzer.linkedProjects": [
567 "./Cargo.toml",
568 "./trident-tests/Cargo.toml"
569 ]
570}"#;
571
572 create_or_update_json_file(&root, &settings_path, new_content)
574 .await
575 .unwrap();
576
577 let backup_path = settings_path.with_extension("json.backup");
579 assert!(!backup_path.exists());
580
581 let content = std::fs::read_to_string(&settings_path).unwrap();
583 let json: Value = serde_json::from_str(&content).unwrap();
584 assert!(json.get("rust-analyzer.linkedProjects").is_some());
585 }
586}