agpm_cli/utils/
fs.rs

1//! File system utilities for cross-platform file operations
2//!
3//! This module provides safe, atomic file operations designed to work consistently
4//! across Windows, macOS, and Linux. All functions handle platform-specific
5//! differences such as path lengths, permissions, and separators.
6//!
7//! # Key Features
8//!
9//! - **Atomic operations**: Files are written atomically to prevent corruption
10//! - **Cross-platform**: Handles Windows long paths, Unix permissions, and path separators
11//! - **Parallel operations**: Async versions for processing multiple files concurrently
12//! - **Safety**: Path traversal prevention and safe path handling
13//! - **Checksum validation**: SHA-256 checksums for data integrity
14//!
15//! # Examples
16//!
17//! ```rust,no_run
18//! use agpm_cli::utils::fs::{ensure_dir, safe_write, calculate_checksum};
19//! use std::path::Path;
20//!
21//! # fn example() -> anyhow::Result<()> {
22//! // Create directory structure
23//! ensure_dir(Path::new("output/agents"))?;
24//!
25//! // Write file atomically
26//! safe_write(Path::new("output/config.toml"), "[sources]")?;
27//!
28//! // Verify file integrity
29//! let checksum = calculate_checksum(Path::new("output/config.toml"))?;
30//! println!("File checksum: {}", checksum);
31//! # Ok(())
32//! # }
33//! ```
34//!
35//! # Platform Considerations
36//!
37//! ## Windows
38//! - Supports long paths (>260 characters) using UNC prefixes
39//! - Handles case-insensitive file systems
40//! - Manages file permissions and attributes correctly
41//!
42//! ## Unix/Linux
43//! - Preserves file permissions during copy operations
44//! - Handles case-sensitive file systems
45//! - Supports symbolic links appropriately
46//!
47//! ## macOS
48//! - Handles HFS+ case-insensitive by default
49//! - Supports extended attributes
50//! - Works with case-sensitive APFS volumes
51
52use crate::core::file_error::{FileOperation, FileResultExt};
53use anyhow::{Context, Result};
54use futures::future::try_join_all;
55use sha2::{Digest, Sha256};
56use std::fs;
57use std::path::{Path, PathBuf};
58
59/// Ensures a directory exists, creating it and all parent directories if necessary.
60///
61/// This function is cross-platform and handles:
62/// - Windows long paths (>260 characters) automatically
63/// - Permission errors with helpful error messages
64/// - Existing files at the target path (returns error)
65///
66/// # Arguments
67///
68/// * `path` - The directory path to create
69///
70/// # Returns
71///
72/// - `Ok(())` if the directory exists or was successfully created
73/// - `Err` if the path exists but is not a directory, or creation fails
74///
75/// # Examples
76///
77/// ```rust,no_run
78/// use agpm_cli::utils::fs::ensure_dir;
79/// use std::path::Path;
80///
81/// # fn example() -> anyhow::Result<()> {
82/// // Create nested directories
83/// ensure_dir(Path::new("output/agents/subdir"))?;
84/// # Ok(())
85/// # }
86/// ```
87///
88/// # Platform Notes
89///
90/// - **Windows**: Automatically handles long paths and provides specific error guidance
91/// - **Unix**: Respects umask for directory permissions
92/// - **All platforms**: Creates parent directories recursively
93pub fn ensure_dir(path: &Path) -> Result<()> {
94    // Handle Windows long paths
95    let safe_path = crate::utils::platform::windows_long_path(path);
96
97    if !safe_path.exists() {
98        fs::create_dir_all(&safe_path).with_context(|| {
99            let platform_help = if crate::utils::platform::is_windows() {
100                "On Windows: Check that the path length is < 260 chars or that long path support is enabled"
101            } else {
102                "Check directory permissions and path validity"
103            };
104
105            format!("Failed to create directory: {}\\n\\n{}", path.display(), platform_help)
106        })?;
107    } else if !safe_path.is_dir() {
108        return Err(anyhow::anyhow!("Path exists but is not a directory: {}", path.display()));
109    }
110    Ok(())
111}
112
113/// Safely writes a string to a file using atomic operations.
114///
115/// This is a convenience wrapper around [`atomic_write`] that handles string-to-bytes conversion.
116/// The write is atomic, meaning the file either contains the new content or the old content,
117/// never a partial write.
118///
119/// # Arguments
120///
121/// * `path` - The file path to write to
122/// * `content` - The string content to write
123///
124/// # Returns
125///
126/// - `Ok(())` if the file was written successfully
127/// - `Err` if the write operation fails
128///
129/// # Examples
130///
131/// ```rust,no_run
132/// use agpm_cli::utils::fs::safe_write;
133/// use std::path::Path;
134///
135/// # fn example() -> anyhow::Result<()> {
136/// safe_write(Path::new("config.toml"), "[sources]\ncommunity = \"https://example.com\"")?;
137/// # Ok(())
138/// # }
139/// ```
140///
141/// # See Also
142///
143/// - [`atomic_write`] for writing raw bytes
144/// - [`atomic_write_multiple`] for batch writing multiple files
145pub fn safe_write(path: &Path, content: &str) -> Result<()> {
146    atomic_write(path, content.as_bytes())
147}
148
149/// Atomically writes bytes to a file using a write-then-rename strategy.
150///
151/// This function ensures atomic writes by:
152/// 1. Writing content to a temporary file (`.tmp` extension)
153/// 2. Syncing the temporary file to disk
154/// 3. Atomically renaming the temporary file to the target path
155///
156/// This approach prevents data corruption from interrupted writes and ensures
157/// readers never see partially written files.
158///
159/// # Arguments
160///
161/// * `path` - The target file path
162/// * `content` - The raw bytes to write
163///
164/// # Returns
165///
166/// - `Ok(())` if the file was written atomically
167/// - `Err` if any step of the atomic write fails
168///
169/// # Examples
170///
171/// ```rust,no_run
172/// use agpm_cli::utils::fs::atomic_write;
173/// use std::path::Path;
174///
175/// # fn example() -> anyhow::Result<()> {
176/// let config_bytes = b"[sources]\ncommunity = \"https://example.com\"";
177/// atomic_write(Path::new("agpm.toml"), config_bytes)?;
178/// # Ok(())
179/// # }
180/// ```
181///
182/// # Platform Notes
183///
184/// - **Windows**: Handles long paths and provides specific error messages
185/// - **Unix**: Preserves file permissions on existing files
186/// - **All platforms**: Creates parent directories if they don't exist
187///
188/// # Guarantees
189///
190/// - **Atomicity**: File contents are never in a partial state
191/// - **Durability**: Content is synced to disk before rename
192/// - **Safety**: Parent directories are created automatically
193pub fn atomic_write(path: &Path, content: &[u8]) -> Result<()> {
194    use std::io::Write;
195
196    // Handle Windows long paths
197    let safe_path = crate::utils::platform::windows_long_path(path);
198
199    // Create parent directory if needed
200    if let Some(parent) = safe_path.parent() {
201        ensure_dir(parent)?;
202    }
203
204    // Write to temporary file first
205    let temp_path = safe_path.with_extension("tmp");
206
207    {
208        let mut file = fs::File::create(&temp_path).with_context(|| {
209            let platform_help = if crate::utils::platform::is_windows() {
210                "On Windows: Check file permissions, path length, and that directory exists"
211            } else {
212                "Check file permissions and that directory exists"
213            };
214
215            format!("Failed to create temp file: {}\\n\\n{}", temp_path.display(), platform_help)
216        })?;
217
218        file.write_all(content)
219            .with_context(|| format!("Failed to write to temp file: {}", temp_path.display()))?;
220
221        file.sync_all().with_context(|| "Failed to sync file to disk")?;
222    }
223
224    // Atomic rename
225    fs::rename(&temp_path, &safe_path)
226        .with_context(|| format!("Failed to rename temp file to: {}", safe_path.display()))?;
227
228    Ok(())
229}
230
231/// Recursively copies a directory and all its contents to a new location.
232///
233/// This function performs a deep copy of all files and subdirectories from the source
234/// to the destination. It creates the destination directory if it doesn't exist and
235/// preserves the directory structure.
236///
237/// # Arguments
238///
239/// * `src` - The source directory to copy from
240/// * `dst` - The destination directory to copy to
241///
242/// # Returns
243///
244/// - `Ok(())` if the directory was copied successfully
245/// - `Err` if the copy operation fails for any file or directory
246///
247/// # Examples
248///
249/// ```rust,no_run
250/// use agpm_cli::utils::fs::copy_dir;
251/// use std::path::Path;
252///
253/// # fn example() -> anyhow::Result<()> {
254/// // Copy entire agent directory
255/// copy_dir(Path::new("cache/agents"), Path::new("output/agents"))?;
256/// # Ok(())
257/// # }
258/// ```
259///
260/// # Behavior
261///
262/// - Creates destination directory if it doesn't exist
263/// - Recursively copies all subdirectories
264/// - Copies only regular files (skips symlinks and special files)
265/// - Overwrites existing files in the destination
266///
267/// # Platform Notes
268///
269/// - **Windows**: Handles long paths and preserves attributes
270/// - **Unix**: Preserves file permissions during copy
271/// - **All platforms**: Does not follow symbolic links
272///
273/// # See Also
274///
275/// - [`copy_dirs_parallel`] for copying multiple directories concurrently
276/// - [`copy_files_parallel`] for batch file copying
277pub fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
278    ensure_dir(dst)?;
279
280    for entry in
281        fs::read_dir(src).with_context(|| format!("Failed to read directory: {}", src.display()))?
282    {
283        let entry = entry?;
284        let file_type = entry.file_type()?;
285        let src_path = entry.path();
286        let dst_path = dst.join(entry.file_name());
287
288        if file_type.is_dir() {
289            copy_dir(&src_path, &dst_path)?;
290        } else if file_type.is_file() {
291            fs::copy(&src_path, &dst_path).with_context(|| {
292                format!("Failed to copy file from {} to {}", src_path.display(), dst_path.display())
293            })?;
294        }
295        // Skip symlinks and other file types
296    }
297
298    Ok(())
299}
300
301/// Recursively removes a directory and all its contents.
302///
303/// This function safely removes a directory tree, handling the case where the
304/// directory doesn't exist (no error). It's designed to be safe for cleanup
305/// operations where the directory may or may not exist.
306///
307/// # Arguments
308///
309/// * `path` - The directory to remove
310///
311/// # Returns
312///
313/// - `Ok(())` if the directory was removed or didn't exist
314/// - `Err` if the removal failed due to permissions or other filesystem errors
315///
316/// # Examples
317///
318/// ```rust,no_run
319/// use agpm_cli::utils::fs::remove_dir_all;
320/// use std::path::Path;
321///
322/// # fn example() -> anyhow::Result<()> {
323/// // Safe cleanup - won't error if directory doesn't exist
324/// remove_dir_all(Path::new("temp/cache"))?;
325/// # Ok(())
326/// # }
327/// ```
328///
329/// # Safety
330///
331/// - Does not follow symbolic links outside the directory tree
332/// - Handles permission errors with descriptive messages
333/// - Safe to call on non-existent directories
334///
335/// # Platform Notes
336///
337/// - **Windows**: Handles long paths and readonly files
338/// - **Unix**: Respects file permissions
339/// - **All platforms**: Atomic operation where supported by filesystem
340pub fn remove_dir_all(path: &Path) -> Result<()> {
341    if path.exists() {
342        fs::remove_dir_all(path)
343            .with_context(|| format!("Failed to remove directory: {}", path.display()))?;
344    }
345    Ok(())
346}
347
348/// Normalizes a path by resolving `.` and `..` components.
349///
350/// This function cleans up path components by:
351/// - Removing `.` (current directory) components
352/// - Resolving `..` (parent directory) components
353/// - Maintaining the path's absolute or relative nature
354///
355/// Note: This function performs logical path resolution without accessing the filesystem.
356/// It does not resolve symbolic links or verify that the path exists.
357///
358/// # Arguments
359///
360/// * `path` - The path to normalize
361///
362/// # Returns
363///
364/// A normalized [`PathBuf`] with `.` and `..` components resolved
365///
366/// # Examples
367///
368/// ```rust,no_run
369/// use agpm_cli::utils::fs::normalize_path;
370/// use std::path::{Path, PathBuf};
371///
372/// let path = Path::new("/foo/./bar/../baz");
373/// let normalized = normalize_path(path);
374/// assert_eq!(normalized, PathBuf::from("/foo/baz"));
375///
376/// let relative = Path::new("../src/./lib.rs");
377/// let normalized_rel = normalize_path(relative);
378/// assert_eq!(normalized_rel, PathBuf::from("../src/lib.rs"));
379/// ```
380///
381/// # Use Cases
382///
383/// - Cleaning user input paths
384/// - Path comparison and deduplication
385/// - Security checks for path traversal
386/// - Canonical path representation
387///
388/// # See Also
389///
390/// - `is_safe_path` for security validation using this normalization
391/// - `safe_canonicalize` for filesystem-aware path resolution
392#[must_use]
393pub fn normalize_path(path: &Path) -> PathBuf {
394    let mut components = Vec::new();
395
396    for component in path.components() {
397        match component {
398            std::path::Component::CurDir => {} // Skip .
399            std::path::Component::ParentDir => {
400                components.pop(); // Remove previous component for ..
401            }
402            c => components.push(c),
403        }
404    }
405
406    components.iter().collect()
407}
408
409/// Checks if a path is safe and doesn't escape the base directory.
410///
411/// This function prevents directory traversal attacks by ensuring that the resolved
412/// path remains within the base directory. It handles both absolute and relative paths,
413/// normalizing them before comparison.
414///
415/// # Arguments
416///
417/// * `base` - The base directory that should contain the path
418/// * `path` - The path to validate (can be absolute or relative)
419///
420/// # Returns
421///
422/// - `true` if the path is safe and stays within the base directory
423/// - `false` if the path would escape the base directory
424///
425/// # Examples
426///
427/// ```rust,no_run
428/// use agpm_cli::utils::fs::is_safe_path;
429/// use std::path::Path;
430///
431/// let base = Path::new("/home/user/project");
432///
433/// // Safe paths
434/// assert!(is_safe_path(base, Path::new("src/main.rs")));
435/// assert!(is_safe_path(base, Path::new("./config/settings.toml")));
436///
437/// // Unsafe paths (directory traversal)
438/// assert!(!is_safe_path(base, Path::new("../../../etc/passwd")));
439/// assert!(!is_safe_path(base, Path::new("/etc/passwd")));
440/// ```
441///
442/// # Security
443///
444/// This function is essential for preventing directory traversal vulnerabilities
445/// when processing user-provided paths. It should be used whenever:
446/// - Extracting archives or packages
447/// - Processing configuration files with path references
448/// - Handling user input that specifies file locations
449///
450/// # Implementation
451///
452/// The function normalizes both paths using `normalize_path` before comparison,
453/// ensuring that path traversal attempts using `../` are properly detected.
454#[must_use]
455pub fn is_safe_path(base: &Path, path: &Path) -> bool {
456    let normalized_base = normalize_path(base);
457    let normalized_path = if path.is_absolute() {
458        normalize_path(path)
459    } else {
460        normalize_path(&base.join(path))
461    };
462
463    normalized_path.starts_with(normalized_base)
464}
465
466/// Recursively finds files matching a pattern in a directory tree.
467///
468/// This function performs a recursive search through the directory tree,
469/// matching files whose names contain the specified pattern. The search
470/// is case-sensitive and uses simple string matching (not regex).
471///
472/// # Arguments
473///
474/// * `dir` - The directory to search in
475/// * `pattern` - The pattern to match in filenames (substring match)
476///
477/// # Returns
478///
479/// A vector of [`PathBuf`]s for all matching files, or an error if the directory
480/// cannot be read.
481///
482/// # Examples
483///
484/// ```rust,no_run
485/// use agpm_cli::utils::fs::find_files;
486/// use std::path::Path;
487///
488/// # fn example() -> anyhow::Result<()> {
489/// // Find all Rust source files
490/// let rust_files = find_files(Path::new("src"), ".rs")?;
491///
492/// // Find all markdown files
493/// let md_files = find_files(Path::new("docs"), ".md")?;
494///
495/// // Find files with "test" in the name
496/// let test_files = find_files(Path::new("."), "test")?;
497/// # Ok(())
498/// # }
499/// ```
500///
501/// # Behavior
502///
503/// - Searches recursively through all subdirectories
504/// - Only returns regular files (not directories or symlinks)
505/// - Uses substring matching (case-sensitive)
506/// - Returns empty vector if no matches found
507/// - Continues searching even if some subdirectories are inaccessible
508///
509/// # Performance
510///
511/// For large directory trees or when searching for many different patterns,
512/// consider using external tools like `fd` or implementing caching for repeated searches.
513pub fn find_files(dir: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
514    let mut files = Vec::new();
515    find_files_recursive(dir, pattern, &mut files)?;
516    Ok(files)
517}
518
519fn find_files_recursive(dir: &Path, pattern: &str, files: &mut Vec<PathBuf>) -> Result<()> {
520    if !dir.is_dir() {
521        return Ok(());
522    }
523
524    for entry in fs::read_dir(dir)? {
525        let entry = entry?;
526        let path = entry.path();
527
528        if path.is_dir() {
529            find_files_recursive(&path, pattern, files)?;
530        } else if path.is_file()
531            && let Some(name) = path.file_name()
532            && name.to_string_lossy().contains(pattern)
533        {
534            files.push(path);
535        }
536    }
537
538    Ok(())
539}
540
541/// Calculates the total size of a directory and all its contents recursively.
542///
543/// This function traverses the directory tree and sums the sizes of all regular files.
544/// It handles nested directories and provides the total disk usage for the directory tree.
545///
546/// # Arguments
547///
548/// * `path` - The directory to calculate size for
549///
550/// # Returns
551///
552/// The total size in bytes, or an error if the directory cannot be read
553///
554/// # Examples
555///
556/// ```rust,no_run
557/// use agpm_cli::utils::fs::dir_size;
558/// use std::path::Path;
559///
560/// # fn example() -> anyhow::Result<()> {
561/// let cache_size = dir_size(Path::new("~/.agpm/cache"))?;
562/// println!("Cache size: {} bytes ({:.2} MB)", cache_size, cache_size as f64 / 1024.0 / 1024.0);
563/// # Ok(())
564/// # }
565/// ```
566///
567/// # Behavior
568///
569/// - Recursively traverses all subdirectories
570/// - Includes only regular files in size calculation
571/// - Does not follow symbolic links
572/// - Returns 0 for empty directories
573/// - Accumulates sizes using 64-bit integers (supports very large directories)
574///
575/// # Performance
576///
577/// This is a synchronous operation that may take time for large directory trees.
578/// For better performance with large directories, use [`get_directory_size`] which
579/// runs the calculation on a separate thread.
580///
581/// # See Also
582///
583/// - [`get_directory_size`] for async version
584/// - Platform-specific tools may be faster for very large directories
585pub fn dir_size(path: &Path) -> Result<u64> {
586    let mut size = 0;
587
588    for entry in fs::read_dir(path)? {
589        let entry = entry?;
590        let metadata = entry.metadata()?;
591
592        if metadata.is_dir() {
593            size += dir_size(&entry.path())?;
594        } else {
595            size += metadata.len();
596        }
597    }
598
599    Ok(size)
600}
601
602/// Asynchronously calculates the total size of a directory and all its contents.
603///
604/// This is the async version of [`dir_size`] that runs the calculation on a separate
605/// thread to avoid blocking the async runtime. Use this when calculating directory
606/// sizes as part of async operations.
607///
608/// # Arguments
609///
610/// * `path` - The directory to calculate size for
611///
612/// # Returns
613///
614/// The total size in bytes, or an error if the operation fails
615///
616/// # Examples
617///
618/// ```rust,no_run
619/// use agpm_cli::utils::fs::get_directory_size;
620/// use std::path::Path;
621///
622/// # async fn example() -> anyhow::Result<()> {
623/// let cache_size = get_directory_size(Path::new("~/.agpm/cache")).await?;
624/// println!("Cache size: {} bytes", cache_size);
625/// # Ok(())
626/// # }
627/// ```
628///
629/// # Performance
630///
631/// This function uses `tokio::task::spawn_blocking` to run the directory traversal
632/// on a thread pool, preventing it from blocking other async tasks. This is particularly
633/// useful when:
634/// - Calculating sizes for multiple directories concurrently
635/// - Integrating with async workflows
636/// - Avoiding blocking in async web servers or CLI applications
637///
638/// # See Also
639///
640/// - [`dir_size`] for synchronous version
641pub async fn get_directory_size(path: &Path) -> Result<u64> {
642    let path = path.to_path_buf();
643    tokio::task::spawn_blocking(move || dir_size(&path))
644        .await
645        .context("Failed to join directory size calculation task")?
646}
647
648/// Ensures that the parent directory of a file path exists.
649///
650/// This is a convenience function for creating the directory structure needed
651/// for a file before writing to it. It extracts the parent directory from the
652/// file path and ensures it exists.
653///
654/// # Arguments
655///
656/// * `path` - The file path whose parent directory should exist
657///
658/// # Returns
659///
660/// - `Ok(())` if the parent directory exists or was created successfully
661/// - `Err` if directory creation fails
662/// - `Ok(())` if the path has no parent (e.g., root level files)
663///
664/// # Examples
665///
666/// ```rust,no_run
667/// use agpm_cli::utils::fs::ensure_parent_dir;
668/// use std::path::Path;
669///
670/// # fn example() -> anyhow::Result<()> {
671/// // Ensure directory structure exists before writing file
672/// ensure_parent_dir(Path::new("output/agents/example.md"))?;
673/// std::fs::write("output/agents/example.md", "# Example Agent")?;
674/// # Ok(())
675/// # }
676/// ```
677///
678/// # Use Cases
679///
680/// - Preparing directory structure before file operations
681/// - Ensuring atomic writes have proper directory structure
682/// - Setting up output paths in batch processing
683///
684/// # See Also
685///
686/// - [`ensure_dir`] for creating a specific directory
687/// - [`atomic_write`] which calls this internally
688pub fn ensure_parent_dir(path: &Path) -> Result<()> {
689    if let Some(parent) = path.parent() {
690        ensure_dir(parent)?;
691    }
692    Ok(())
693}
694
695/// Alias for `ensure_dir` for consistency
696pub fn ensure_dir_exists(path: &Path) -> Result<()> {
697    ensure_dir(path)
698}
699
700/// Copy a directory recursively (alias for consistency)
701pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
702    copy_dir(src, dst)
703}
704
705/// Finds the AGPM project root by searching for `agpm.toml` in the directory hierarchy.
706///
707/// This function starts from the given directory and walks up the directory tree
708/// looking for a `agpm.toml` file, which indicates the root of a AGPM project.
709/// This is similar to how Git finds the repository root by looking for `.git`.
710///
711/// # Arguments
712///
713/// * `start` - The directory to start searching from (typically current directory)
714///
715/// # Returns
716///
717/// The path to the directory containing `agpm.toml`, or an error if not found
718///
719/// # Examples
720///
721/// ```rust,no_run
722/// use agpm_cli::utils::fs::find_project_root;
723/// use std::env;
724///
725/// # fn example() -> anyhow::Result<()> {
726/// // Find project root from current directory
727/// let current_dir = env::current_dir()?;
728/// let project_root = find_project_root(&current_dir)?;
729/// println!("Project root: {}", project_root.display());
730/// # Ok(())
731/// # }
732/// ```
733///
734/// # Behavior
735///
736/// - Starts from the given directory and searches upward
737/// - Returns the first directory containing `agpm.toml`
738/// - Canonicalizes the starting path to handle symlinks
739/// - Stops at filesystem root if no `agpm.toml` is found
740///
741/// # Error Cases
742///
743/// - No `agpm.toml` found in the directory hierarchy
744/// - Permission denied accessing parent directories
745/// - Invalid or inaccessible starting path
746///
747/// # Use Cases
748///
749/// - CLI commands that need to operate on the current project
750/// - Finding configuration files relative to project root
751/// - Validating that commands are run within a AGPM project
752pub fn find_project_root(start: &Path) -> Result<PathBuf> {
753    let mut current = start.canonicalize().unwrap_or_else(|_| start.to_path_buf());
754
755    loop {
756        if current.join("agpm.toml").exists() {
757            return Ok(current);
758        }
759
760        if !current.pop() {
761            return Err(anyhow::anyhow!(
762                "No agpm.toml found in current directory or any parent directory"
763            ));
764        }
765    }
766}
767
768/// Returns the path to the global AGPM configuration file.
769///
770/// This function constructs the path to the global configuration file following
771/// platform conventions. The global config contains user-specific settings like
772/// authentication tokens and private repository URLs.
773///
774/// # Returns
775///
776/// The path to `~/.config/agpm/config.toml`, or an error if the home directory
777/// cannot be determined
778///
779/// # Examples
780///
781/// ```rust,no_run
782/// use agpm_cli::utils::fs::get_global_config_path;
783///
784/// # fn example() -> anyhow::Result<()> {
785/// let config_path = get_global_config_path()?;
786/// println!("Global config at: {}", config_path.display());
787///
788/// // Check if global config exists
789/// if config_path.exists() {
790///     let config = std::fs::read_to_string(&config_path)?;
791///     println!("Config contents: {}", config);
792/// }
793/// # Ok(())
794/// # }
795/// ```
796///
797/// # Platform Paths
798///
799/// - **Linux**: `~/.config/agpm/config.toml`
800/// - **macOS**: `~/.config/agpm/config.toml`
801/// - **Windows**: `%USERPROFILE%\.config\agpm\config.toml`
802///
803/// # Use Cases
804///
805/// - Loading global user configuration
806/// - Storing authentication tokens securely
807/// - Sharing settings across multiple projects
808///
809/// # Security Note
810///
811/// This file may contain sensitive information like API tokens. It should never
812/// be committed to version control or shared publicly.
813pub fn get_global_config_path() -> Result<PathBuf> {
814    let home = crate::utils::platform::get_home_dir()?;
815    Ok(home.join(".config").join("agpm").join("config.toml"))
816}
817
818/// A temporary directory that automatically cleans up when dropped.
819///
820/// This struct provides RAII (Resource Acquisition Is Initialization) semantics
821/// for temporary directories. The directory is created when the struct is created
822/// and automatically removed when the struct is dropped, even if the program panics.
823///
824/// # Examples
825///
826/// ```rust,no_run
827/// use agpm_cli::utils::fs::TempDir;
828///
829/// # fn example() -> anyhow::Result<()> {
830/// {
831///     let temp = TempDir::new("test")?;
832///     let temp_path = temp.path();
833///     
834///     // Use the temporary directory
835///     std::fs::write(temp_path.join("file.txt"), "temporary data")?;
836///     
837///     // Directory exists here
838///     assert!(temp_path.exists());
839/// } // TempDir is dropped here, directory is automatically cleaned up
840/// # Ok(())
841/// # }
842/// ```
843///
844/// # Thread Safety
845///
846/// Each `TempDir` instance creates a unique directory using UUID generation,
847/// making it safe to use across multiple threads without naming conflicts.
848///
849/// # Cleanup Behavior
850///
851/// - Directory is removed recursively when dropped
852/// - Cleanup happens even if the program panics
853/// - If cleanup fails (rare), the error is silently ignored
854/// - Uses the system temporary directory as the parent
855///
856/// # Use Cases
857///
858/// - Unit testing with temporary files
859/// - Staging areas for atomic operations
860/// - Scratch space for temporary processing
861pub struct TempDir {
862    path: PathBuf,
863}
864
865impl TempDir {
866    /// Creates a new temporary directory with the given prefix.
867    ///
868    /// The directory is created immediately and will have a name like
869    /// `agpm_{prefix}_{uuid}` in the system temporary directory.
870    ///
871    /// # Arguments
872    ///
873    /// * `prefix` - A prefix for the directory name (for identification)
874    ///
875    /// # Returns
876    ///
877    /// A new `TempDir` instance, or an error if directory creation fails
878    ///
879    /// # Examples
880    ///
881    /// ```rust,no_run
882    /// use agpm_cli::utils::fs::TempDir;
883    ///
884    /// # fn example() -> anyhow::Result<()> {
885    /// let temp = TempDir::new("cache")?;
886    /// println!("Temporary directory: {}", temp.path().display());
887    /// # Ok(())
888    /// # }
889    /// ```
890    pub fn new(prefix: &str) -> Result<Self> {
891        let temp_dir = std::env::temp_dir();
892        let unique_name = format!("agpm_{}_{}", prefix, uuid::Uuid::new_v4());
893        let path = temp_dir.join(unique_name);
894
895        ensure_dir(&path)?;
896
897        Ok(Self {
898            path,
899        })
900    }
901
902    /// Returns the path to the temporary directory.
903    ///
904    /// The directory is guaranteed to exist as long as this `TempDir` instance exists.
905    ///
906    /// # Returns
907    ///
908    /// A reference to the temporary directory path
909    #[must_use]
910    pub fn path(&self) -> &Path {
911        &self.path
912    }
913}
914
915impl Drop for TempDir {
916    fn drop(&mut self) {
917        let _ = remove_dir_all(&self.path);
918    }
919}
920
921/// Calculates the SHA-256 checksum of a file.
922///
923/// This function reads the entire file into memory and computes its SHA-256 hash,
924/// returning it as a lowercase hexadecimal string. This is useful for verifying
925/// file integrity and detecting changes.
926///
927/// # Arguments
928///
929/// * `path` - The path to the file to checksum
930///
931/// # Returns
932///
933/// A 64-character lowercase hexadecimal string representing the SHA-256 hash,
934/// or an error if the file cannot be read
935///
936/// # Examples
937///
938/// ```rust,no_run
939/// use agpm_cli::utils::fs::calculate_checksum;
940/// use std::path::Path;
941///
942/// # fn example() -> anyhow::Result<()> {
943/// let checksum = calculate_checksum(Path::new("important-file.txt"))?;
944/// println!("File checksum: {}", checksum);
945///
946/// // Verify against expected checksum
947/// let expected = "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2";
948/// if checksum == expected {
949///     println!("File integrity verified!");
950/// }
951/// # Ok(())
952/// # }
953/// ```
954///
955/// # Performance
956///
957/// This function reads the entire file into memory, so it may not be suitable
958/// for very large files. For processing multiple files, consider using
959/// [`calculate_checksums_parallel`] for better performance.
960///
961/// # Security
962///
963/// SHA-256 is cryptographically secure and suitable for:
964/// - Integrity verification
965/// - Change detection
966/// - Digital signatures
967/// - Blockchain applications
968///
969/// # See Also
970///
971/// - [`calculate_checksums_parallel`] for batch processing
972/// - [`hex`] crate for hexadecimal encoding
973pub fn calculate_checksum(path: &Path) -> Result<String> {
974    let content = fs::read(path)
975        .with_context(|| format!("Failed to read file for checksum: {}", path.display()))?;
976
977    let mut hasher = Sha256::new();
978    hasher.update(&content);
979    let result = hasher.finalize();
980
981    Ok(hex::encode(result))
982}
983
984/// Calculates SHA-256 checksums for multiple files concurrently.
985///
986/// This function processes multiple files in parallel using Tokio's thread pool,
987/// which can significantly improve performance when processing many files or
988/// large files on systems with multiple CPU cores.
989///
990/// # Arguments
991///
992/// * `paths` - A slice of file paths to process
993///
994/// # Returns
995///
996/// A vector of tuples containing each file path and its corresponding checksum,
997/// in the same order as the input paths. Returns an error if any file fails
998/// to be processed.
999///
1000/// # Examples
1001///
1002/// ```rust,no_run
1003/// use agpm_cli::utils::fs::calculate_checksums_parallel;
1004/// use std::path::PathBuf;
1005///
1006/// # async fn example() -> anyhow::Result<()> {
1007/// let files = vec![
1008///     PathBuf::from("file1.txt"),
1009///     PathBuf::from("file2.txt"),
1010///     PathBuf::from("file3.txt"),
1011/// ];
1012///
1013/// let results = calculate_checksums_parallel(&files).await?;
1014/// for (path, checksum) in results {
1015///     println!("{}: {}", path.display(), checksum);
1016/// }
1017/// # Ok(())
1018/// # }
1019/// ```
1020///
1021/// # Performance
1022///
1023/// This function uses `tokio::task::spawn_blocking` to run checksum calculations
1024/// on separate threads, allowing for true parallelism. Benefits:
1025/// - CPU-bound work doesn't block the async runtime
1026/// - Multiple files processed simultaneously
1027/// - Scales with available CPU cores
1028/// - Maintains order of results
1029///
1030/// # Error Handling
1031///
1032/// If any file fails to be processed, the entire operation fails and returns
1033/// an error with details about all failures. This "all-or-nothing" approach
1034/// ensures data consistency.
1035///
1036/// # See Also
1037///
1038/// - [`calculate_checksum`] for single file processing
1039/// - [`read_files_parallel`] for concurrent file reading
1040pub async fn calculate_checksums_parallel(paths: &[PathBuf]) -> Result<Vec<(PathBuf, String)>> {
1041    if paths.is_empty() {
1042        return Ok(Vec::new());
1043    }
1044
1045    let mut tasks = Vec::new();
1046
1047    for (index, path) in paths.iter().enumerate() {
1048        let path = path.clone();
1049        let task = tokio::task::spawn_blocking(move || {
1050            calculate_checksum(&path).map(|checksum| (index, path, checksum))
1051        });
1052        tasks.push(task);
1053    }
1054
1055    let results = try_join_all(tasks).await.context("Failed to join checksum calculation tasks")?;
1056
1057    let mut successes = Vec::new();
1058    let mut errors = Vec::new();
1059
1060    for result in results {
1061        match result {
1062            Ok((index, path, checksum)) => successes.push((index, path, checksum)),
1063            Err(e) => errors.push(e),
1064        }
1065    }
1066
1067    if !errors.is_empty() {
1068        let error_msgs: Vec<String> =
1069            errors.into_iter().map(|error| format!("  {error}")).collect();
1070        return Err(anyhow::anyhow!(
1071            "Failed to calculate checksums for {} files:\n{}",
1072            error_msgs.len(),
1073            error_msgs.join("\n")
1074        ));
1075    }
1076
1077    // Sort results by original index to maintain order
1078    successes.sort_by_key(|(index, _, _)| *index);
1079    let ordered_results: Vec<(PathBuf, String)> =
1080        successes.into_iter().map(|(_, path, checksum)| (path, checksum)).collect();
1081
1082    Ok(ordered_results)
1083}
1084
1085/// Copies multiple files concurrently using parallel processing.
1086///
1087/// This function performs multiple file copy operations in parallel, which can
1088/// significantly improve performance when copying many files, especially on
1089/// systems with fast storage and multiple CPU cores.
1090///
1091/// # Arguments
1092///
1093/// * `sources_and_destinations` - A slice of (source, destination) path pairs
1094///
1095/// # Returns
1096///
1097/// - `Ok(())` if all files were copied successfully
1098/// - `Err` if any copy operation fails, with details about all failures
1099///
1100/// # Examples
1101///
1102/// ```rust,no_run
1103/// use agpm_cli::utils::fs::copy_files_parallel;
1104/// use std::path::PathBuf;
1105///
1106/// # async fn example() -> anyhow::Result<()> {
1107/// let copy_operations = vec![
1108///     (PathBuf::from("src/agent1.md"), PathBuf::from("output/agent1.md")),
1109///     (PathBuf::from("src/agent2.md"), PathBuf::from("output/agent2.md")),
1110///     (PathBuf::from("src/snippet.md"), PathBuf::from("output/snippet.md")),
1111/// ];
1112///
1113/// copy_files_parallel(&copy_operations).await?;
1114/// println!("All files copied successfully!");
1115/// # Ok(())
1116/// # }
1117/// ```
1118///
1119/// # Features
1120///
1121/// - **Parallel execution**: Uses thread pool for concurrent operations
1122/// - **Automatic directory creation**: Creates destination directories as needed
1123/// - **Atomic behavior**: Either all files copy successfully or none do
1124/// - **Progress tracking**: Can be combined with progress bars for user feedback
1125///
1126/// # Performance Characteristics
1127///
1128/// - Best for many small to medium files
1129/// - Scales with available CPU cores and I/O bandwidth
1130/// - May not improve performance for very large files (I/O bound)
1131/// - Respects filesystem limits on concurrent operations
1132///
1133/// # Error Handling
1134///
1135/// All copy operations must succeed for the function to return `Ok(())`. If any
1136/// operation fails, detailed error information is provided for troubleshooting.
1137///
1138/// # See Also
1139///
1140/// - [`copy_dirs_parallel`] for directory copying
1141/// - [`atomic_write_multiple`] for writing multiple files
1142pub async fn copy_files_parallel(sources_and_destinations: &[(PathBuf, PathBuf)]) -> Result<()> {
1143    if sources_and_destinations.is_empty() {
1144        return Ok(());
1145    }
1146
1147    let mut tasks = Vec::new();
1148
1149    for (src, dst) in sources_and_destinations {
1150        let src = src.clone();
1151        let dst = dst.clone();
1152        let task = tokio::task::spawn_blocking(move || {
1153            // Ensure destination directory exists
1154            if let Some(parent) = dst.parent() {
1155                ensure_dir(parent)?;
1156            }
1157
1158            // Copy file
1159            fs::copy(&src, &dst).with_context(|| {
1160                format!("Failed to copy file from {} to {}", src.display(), dst.display())
1161            })?;
1162
1163            Ok::<_, anyhow::Error>((src, dst))
1164        });
1165        tasks.push(task);
1166    }
1167
1168    let results = try_join_all(tasks).await.context("Failed to join file copy tasks")?;
1169
1170    let mut errors = Vec::new();
1171
1172    for result in results {
1173        if let Err(e) = result {
1174            errors.push(e);
1175        }
1176    }
1177
1178    if !errors.is_empty() {
1179        let error_msgs: Vec<String> =
1180            errors.into_iter().map(|error| format!("  {error}")).collect();
1181        return Err(anyhow::anyhow!(
1182            "Failed to copy {} files:\n{}",
1183            error_msgs.len(),
1184            error_msgs.join("\n")
1185        ));
1186    }
1187
1188    Ok(())
1189}
1190
1191/// Writes multiple files atomically in parallel.
1192///
1193/// This function performs multiple atomic write operations concurrently,
1194/// which can significantly improve performance when writing many files.
1195/// Each file is written atomically using the same write-then-rename strategy
1196/// as [`atomic_write`].
1197///
1198/// # Arguments
1199///
1200/// * `files` - A slice of (path, content) pairs to write
1201///
1202/// # Returns
1203///
1204/// - `Ok(())` if all files were written successfully
1205/// - `Err` if any write operation fails, with details about all failures
1206///
1207/// # Examples
1208///
1209/// ```rust,no_run
1210/// use agpm_cli::utils::fs::atomic_write_multiple;
1211/// use std::path::PathBuf;
1212///
1213/// # async fn example() -> anyhow::Result<()> {
1214/// let files = vec![
1215///     (PathBuf::from("config1.toml"), b"[sources]\ncommunity = \"url1\"".to_vec()),
1216///     (PathBuf::from("config2.toml"), b"[sources]\nprivate = \"url2\"".to_vec()),
1217///     (PathBuf::from("readme.md"), b"# Project Documentation".to_vec()),
1218/// ];
1219///
1220/// atomic_write_multiple(&files).await?;
1221/// println!("All configuration files written atomically!");
1222/// # Ok(())
1223/// # }
1224/// ```
1225///
1226/// # Atomicity Guarantees
1227///
1228/// - Each individual file is written atomically
1229/// - Either all files are written successfully or the operation fails
1230/// - No partially written files are left on disk
1231/// - Parent directories are created automatically
1232///
1233/// # Performance
1234///
1235/// This function uses parallel execution to improve performance:
1236/// - Multiple files written concurrently
1237/// - Scales with available CPU cores and I/O bandwidth
1238/// - Particularly effective for many small files
1239/// - Maintains atomicity guarantees for each file
1240///
1241/// # See Also
1242///
1243/// - [`atomic_write`] for single file atomic writes
1244/// - [`safe_write`] for string content convenience
1245/// - [`copy_files_parallel`] for file copying operations
1246pub async fn atomic_write_multiple(files: &[(PathBuf, Vec<u8>)]) -> Result<()> {
1247    if files.is_empty() {
1248        return Ok(());
1249    }
1250
1251    let mut tasks = Vec::new();
1252
1253    for (path, content) in files {
1254        let path = path.clone();
1255        let content = content.clone();
1256        let task =
1257            tokio::task::spawn_blocking(move || atomic_write(&path, &content).map(|()| path));
1258        tasks.push(task);
1259    }
1260
1261    let results = try_join_all(tasks).await.context("Failed to join atomic write tasks")?;
1262
1263    let mut errors = Vec::new();
1264
1265    for result in results {
1266        if let Err(e) = result {
1267            errors.push(e);
1268        }
1269    }
1270
1271    if !errors.is_empty() {
1272        let error_msgs: Vec<String> =
1273            errors.into_iter().map(|error| format!("  {error}")).collect();
1274        return Err(anyhow::anyhow!(
1275            "Failed to write {} files:\n{}",
1276            error_msgs.len(),
1277            error_msgs.join("\n")
1278        ));
1279    }
1280
1281    Ok(())
1282}
1283
1284/// Copies multiple directories concurrently.
1285///
1286/// This function performs multiple directory copy operations in parallel,
1287/// which can improve performance when copying several separate directory trees.
1288/// Each directory is copied recursively using the same logic as [`copy_dir`].
1289///
1290/// # Arguments
1291///
1292/// * `sources_and_destinations` - A slice of (source, destination) directory pairs
1293///
1294/// # Returns
1295///
1296/// - `Ok(())` if all directories were copied successfully
1297/// - `Err` if any copy operation fails, with details about all failures
1298///
1299/// # Examples
1300///
1301/// ```rust,no_run
1302/// use agpm_cli::utils::fs::copy_dirs_parallel;
1303/// use std::path::PathBuf;
1304///
1305/// # async fn example() -> anyhow::Result<()> {
1306/// let copy_operations = vec![
1307///     (PathBuf::from("cache/agents"), PathBuf::from("output/agents")),
1308///     (PathBuf::from("cache/snippets"), PathBuf::from("output/snippets")),
1309///     (PathBuf::from("cache/templates"), PathBuf::from("output/templates")),
1310/// ];
1311///
1312/// copy_dirs_parallel(&copy_operations).await?;
1313/// println!("All directories copied successfully!");
1314/// # Ok(())
1315/// # }
1316/// ```
1317///
1318/// # Features
1319///
1320/// - **Recursive copying**: Each directory is copied with all subdirectories
1321/// - **Parallel execution**: Multiple directory trees copied concurrently
1322/// - **Atomic behavior**: Either all directories copy successfully or operation fails
1323/// - **Automatic creation**: Destination directories are created as needed
1324///
1325/// # Use Cases
1326///
1327/// - Copying multiple resource categories simultaneously
1328/// - Batch operations on directory structures
1329/// - Backup operations for multiple directories
1330/// - Installation processes involving multiple components
1331///
1332/// # Performance Considerations
1333///
1334/// - Best performance with multiple separate directory trees
1335/// - May not improve performance if directories share the same disk
1336/// - Memory usage scales with number of directories and their sizes
1337/// - Respects filesystem concurrent operation limits
1338///
1339/// # See Also
1340///
1341/// - [`copy_dir`] for single directory copying
1342/// - [`copy_files_parallel`] for individual file copying
1343pub async fn copy_dirs_parallel(sources_and_destinations: &[(PathBuf, PathBuf)]) -> Result<()> {
1344    if sources_and_destinations.is_empty() {
1345        return Ok(());
1346    }
1347
1348    let mut tasks = Vec::new();
1349
1350    for (src, dst) in sources_and_destinations {
1351        let src = src.clone();
1352        let dst = dst.clone();
1353        let task = tokio::task::spawn_blocking(move || copy_dir(&src, &dst).map(|()| (src, dst)));
1354        tasks.push(task);
1355    }
1356
1357    let results = try_join_all(tasks).await.context("Failed to join directory copy tasks")?;
1358
1359    let mut errors = Vec::new();
1360
1361    for result in results {
1362        if let Err(e) = result {
1363            errors.push(e);
1364        }
1365    }
1366
1367    if !errors.is_empty() {
1368        let error_msgs: Vec<String> =
1369            errors.into_iter().map(|error| format!("  {error}")).collect();
1370        return Err(anyhow::anyhow!(
1371            "Failed to copy {} directories:\n{}",
1372            error_msgs.len(),
1373            error_msgs.join("\n")
1374        ));
1375    }
1376
1377    Ok(())
1378}
1379
1380/// Reads multiple files concurrently and returns their contents.
1381///
1382/// This function reads multiple text files in parallel, which can improve
1383/// performance when processing many files, especially on systems with
1384/// fast storage and multiple CPU cores.
1385///
1386/// # Arguments
1387///
1388/// * `paths` - A slice of file paths to read
1389///
1390/// # Returns
1391///
1392/// A vector of tuples containing each file path and its content as a UTF-8 string,
1393/// in the same order as the input paths. Returns an error if any file fails
1394/// to be read or contains invalid UTF-8.
1395///
1396/// # Examples
1397///
1398/// ```rust,no_run
1399/// use agpm_cli::utils::fs::read_files_parallel;
1400/// use std::path::PathBuf;
1401///
1402/// # async fn example() -> anyhow::Result<()> {
1403/// let config_files = vec![
1404///     PathBuf::from("agpm.toml"),
1405///     PathBuf::from("agents/agent1.md"),
1406///     PathBuf::from("snippets/snippet1.md"),
1407/// ];
1408///
1409/// let results = read_files_parallel(&config_files).await?;
1410/// for (path, content) in results {
1411///     println!("{}: {} characters", path.display(), content.len());
1412/// }
1413/// # Ok(())
1414/// # }
1415/// ```
1416///
1417/// # Performance
1418///
1419/// This function uses `tokio::task::spawn_blocking` to perform file I/O
1420/// on separate threads:
1421/// - Multiple files read simultaneously
1422/// - Non-blocking for the async runtime
1423/// - Scales with available CPU cores and I/O bandwidth
1424/// - Maintains result ordering
1425///
1426/// # UTF-8 Handling
1427///
1428/// All files must contain valid UTF-8 text. If any file contains invalid
1429/// UTF-8 bytes, the operation will fail with a descriptive error.
1430///
1431/// # Error Handling
1432///
1433/// If any file fails to be read (due to permissions, missing file, or
1434/// invalid UTF-8), the entire operation fails. This ensures consistency
1435/// when processing related files that should all be available.
1436///
1437/// # See Also
1438///
1439/// - [`calculate_checksums_parallel`] for file integrity verification
1440/// - [`atomic_write_multiple`] for batch writing operations
1441pub async fn read_files_parallel(paths: &[PathBuf]) -> Result<Vec<(PathBuf, String)>> {
1442    if paths.is_empty() {
1443        return Ok(Vec::new());
1444    }
1445
1446    let mut tasks = Vec::new();
1447
1448    for (index, path) in paths.iter().enumerate() {
1449        let path = path.clone();
1450        let task = tokio::task::spawn_blocking(move || {
1451            fs::read_to_string(&path).map(|content| (index, path, content))
1452        });
1453        tasks.push(task);
1454    }
1455
1456    let results = try_join_all(tasks).await.context("Failed to join file read tasks")?;
1457
1458    let mut successes = Vec::new();
1459    let mut errors = Vec::new();
1460
1461    for result in results {
1462        match result {
1463            Ok((index, path, content)) => successes.push((index, path, content)),
1464            Err(e) => errors.push(e),
1465        }
1466    }
1467
1468    if !errors.is_empty() {
1469        let error_msgs: Vec<String> =
1470            errors.into_iter().map(|error| format!("  {error}")).collect();
1471        return Err(anyhow::anyhow!(
1472            "Failed to read {} files:\n{}",
1473            error_msgs.len(),
1474            error_msgs.join("\n")
1475        ));
1476    }
1477
1478    // Sort results by original index to maintain order
1479    successes.sort_by_key(|(index, _, _)| *index);
1480    let ordered_results: Vec<(PathBuf, String)> =
1481        successes.into_iter().map(|(_, path, content)| (path, content)).collect();
1482
1483    Ok(ordered_results)
1484}
1485
1486// ============================================================================
1487// Unified File I/O Operations
1488// ============================================================================
1489
1490/// Reads a text file with proper error handling and context.
1491///
1492/// # Arguments
1493/// * `path` - The path to the file to read
1494///
1495/// # Returns
1496/// The contents of the file as a String
1497///
1498/// # Errors
1499/// Returns an error with context if the file cannot be read
1500pub fn read_text_file(path: &Path) -> Result<String> {
1501    Ok(fs::read_to_string(path).with_file_context(
1502        FileOperation::Read,
1503        path,
1504        "reading text file",
1505        "utils::fs::read_text_file",
1506    )?)
1507}
1508
1509/// Writes a text file atomically with proper error handling.
1510///
1511/// # Arguments
1512/// * `path` - The path to write to
1513/// * `content` - The text content to write
1514///
1515/// # Returns
1516/// Ok(()) on success
1517///
1518/// # Errors
1519/// Returns an error with context if the file cannot be written
1520pub fn write_text_file(path: &Path, content: &str) -> Result<()> {
1521    safe_write(path, content).with_context(|| format!("Failed to write file: {}", path.display()))
1522}
1523
1524/// Reads and parses a JSON file.
1525///
1526/// # Arguments
1527/// * `path` - The path to the JSON file
1528///
1529/// # Type Parameters
1530/// * `T` - The type to deserialize into (must implement `DeserializeOwned`)
1531///
1532/// # Returns
1533/// The parsed JSON data
1534///
1535/// # Errors
1536/// Returns an error if the file cannot be read or parsed
1537pub fn read_json_file<T>(path: &Path) -> Result<T>
1538where
1539    T: serde::de::DeserializeOwned,
1540{
1541    let content = read_text_file(path)?;
1542    serde_json::from_str(&content)
1543        .with_context(|| format!("Failed to parse JSON from file: {}", path.display()))
1544}
1545
1546/// Writes data as JSON to a file atomically.
1547///
1548/// # Arguments
1549/// * `path` - The path to write to
1550/// * `data` - The data to serialize
1551/// * `pretty` - Whether to use pretty formatting
1552///
1553/// # Type Parameters
1554/// * `T` - The type to serialize (must implement Serialize)
1555///
1556/// # Returns
1557/// Ok(()) on success
1558///
1559/// # Errors
1560/// Returns an error if serialization fails or the file cannot be written
1561pub fn write_json_file<T>(path: &Path, data: &T, pretty: bool) -> Result<()>
1562where
1563    T: serde::Serialize,
1564{
1565    let json = if pretty {
1566        serde_json::to_string_pretty(data)?
1567    } else {
1568        serde_json::to_string(data)?
1569    };
1570
1571    write_text_file(path, &json)
1572        .with_context(|| format!("Failed to write JSON file: {}", path.display()))
1573}
1574
1575/// Reads and parses a TOML file.
1576///
1577/// # Arguments
1578/// * `path` - The path to the TOML file
1579///
1580/// # Type Parameters
1581/// * `T` - The type to deserialize into (must implement `DeserializeOwned`)
1582///
1583/// # Returns
1584/// The parsed TOML data
1585///
1586/// # Errors
1587/// Returns an error if the file cannot be read or parsed
1588pub fn read_toml_file<T>(path: &Path) -> Result<T>
1589where
1590    T: serde::de::DeserializeOwned,
1591{
1592    let content = read_text_file(path)?;
1593    toml::from_str(&content)
1594        .with_context(|| format!("Failed to parse TOML from file: {}", path.display()))
1595}
1596
1597/// Writes data as TOML to a file atomically.
1598///
1599/// # Arguments
1600/// * `path` - The path to write to
1601/// * `data` - The data to serialize
1602/// * `pretty` - Whether to use pretty formatting (always true for TOML)
1603///
1604/// # Type Parameters
1605/// * `T` - The type to serialize (must implement Serialize)
1606///
1607/// # Returns
1608/// Ok(()) on success
1609///
1610/// # Errors
1611/// Returns an error if serialization fails or the file cannot be written
1612pub fn write_toml_file<T>(path: &Path, data: &T) -> Result<()>
1613where
1614    T: serde::Serialize,
1615{
1616    let toml = toml::to_string_pretty(data)
1617        .with_context(|| format!("Failed to serialize data to TOML for: {}", path.display()))?;
1618
1619    write_text_file(path, &toml)
1620        .with_context(|| format!("Failed to write TOML file: {}", path.display()))
1621}
1622
1623/// Reads and parses a YAML file.
1624///
1625/// # Arguments
1626/// * `path` - The path to the YAML file
1627///
1628/// # Type Parameters
1629/// * `T` - The type to deserialize into (must implement `DeserializeOwned`)
1630///
1631/// # Returns
1632/// The parsed YAML data
1633///
1634/// # Errors
1635/// Returns an error if the file cannot be read or parsed
1636pub fn read_yaml_file<T>(path: &Path) -> Result<T>
1637where
1638    T: serde::de::DeserializeOwned,
1639{
1640    let content = read_text_file(path)?;
1641    serde_yaml::from_str(&content)
1642        .with_context(|| format!("Failed to parse YAML from file: {}", path.display()))
1643}
1644
1645/// Writes data as YAML to a file atomically.
1646///
1647/// # Arguments
1648/// * `path` - The path to write to
1649/// * `data` - The data to serialize
1650///
1651/// # Type Parameters
1652/// * `T` - The type to serialize (must implement Serialize)
1653///
1654/// # Returns
1655/// Ok(()) on success
1656///
1657/// # Errors
1658/// Returns an error if serialization fails or the file cannot be written
1659pub fn write_yaml_file<T>(path: &Path, data: &T) -> Result<()>
1660where
1661    T: serde::Serialize,
1662{
1663    let yaml = serde_yaml::to_string(data)
1664        .with_context(|| format!("Failed to serialize data to YAML for: {}", path.display()))?;
1665
1666    write_text_file(path, &yaml)
1667        .with_context(|| format!("Failed to write YAML file: {}", path.display()))
1668}
1669
1670/// Creates a temporary file with content for testing.
1671///
1672/// # Arguments
1673/// * `prefix` - The prefix for the temp file name
1674/// * `content` - The content to write to the file
1675///
1676/// # Returns
1677/// A `TempPath` that will delete the file when dropped
1678///
1679/// # Errors
1680/// Returns an error if the temp file cannot be created
1681pub fn create_temp_file(prefix: &str, content: &str) -> Result<tempfile::TempPath> {
1682    let temp_file = tempfile::Builder::new().prefix(prefix).suffix(".tmp").tempfile()?;
1683
1684    let path = temp_file.into_temp_path();
1685    write_text_file(&path, content)?;
1686
1687    Ok(path)
1688}
1689
1690/// Checks if a file exists and is readable.
1691///
1692/// # Arguments
1693/// * `path` - The path to check
1694///
1695/// # Returns
1696/// true if the file exists and is readable, false otherwise
1697pub fn file_exists_and_readable(path: &Path) -> bool {
1698    path.exists() && path.is_file() && fs::metadata(path).is_ok()
1699}
1700
1701/// Gets the modification time of a file.
1702///
1703/// # Arguments
1704/// * `path` - The path to the file
1705///
1706/// # Returns
1707/// The modification time as a `SystemTime`
1708///
1709/// # Errors
1710/// Returns an error if the file metadata cannot be read
1711pub fn get_modified_time(path: &Path) -> Result<std::time::SystemTime> {
1712    let metadata = fs::metadata(path)
1713        .with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
1714
1715    metadata
1716        .modified()
1717        .with_context(|| format!("Failed to get modification time for: {}", path.display()))
1718}
1719
1720/// Compares the modification times of two files.
1721///
1722/// # Arguments
1723/// * `path1` - The first file path
1724/// * `path2` - The second file path
1725///
1726/// # Returns
1727/// - `Ok(Ordering::Less)` if path1 is older than path2
1728/// - `Ok(Ordering::Greater)` if path1 is newer than path2
1729/// - `Ok(Ordering::Equal)` if they have the same modification time
1730///
1731/// # Errors
1732/// Returns an error if either file's metadata cannot be read
1733pub fn compare_file_times(path1: &Path, path2: &Path) -> Result<std::cmp::Ordering> {
1734    let time1 = get_modified_time(path1)?;
1735    let time2 = get_modified_time(path2)?;
1736
1737    Ok(time1.cmp(&time2))
1738}
1739
1740#[cfg(test)]
1741mod tests {
1742    use super::*;
1743    use tempfile::tempdir;
1744
1745    #[test]
1746    fn test_ensure_dir() {
1747        let temp = tempdir().unwrap();
1748        let test_dir = temp.path().join("test_dir");
1749
1750        assert!(!test_dir.exists());
1751        ensure_dir(&test_dir).unwrap();
1752        assert!(test_dir.exists());
1753        assert!(test_dir.is_dir());
1754    }
1755
1756    #[test]
1757    fn test_normalize_path() {
1758        let path = Path::new("/foo/./bar/../baz");
1759        let normalized = normalize_path(path);
1760        assert_eq!(normalized, PathBuf::from("/foo/baz"));
1761    }
1762
1763    #[test]
1764    fn test_is_safe_path() {
1765        let base = Path::new("/home/user/project");
1766
1767        assert!(is_safe_path(base, Path::new("subdir/file.txt")));
1768        assert!(is_safe_path(base, Path::new("./subdir/file.txt")));
1769        assert!(!is_safe_path(base, Path::new("../other/file.txt")));
1770        assert!(!is_safe_path(base, Path::new("/etc/passwd")));
1771    }
1772
1773    #[test]
1774    fn test_safe_write() {
1775        let temp = tempdir().unwrap();
1776        let file_path = temp.path().join("test.txt");
1777
1778        safe_write(&file_path, "test content").unwrap();
1779
1780        let content = std::fs::read_to_string(&file_path).unwrap();
1781        assert_eq!(content, "test content");
1782    }
1783
1784    #[test]
1785    fn test_safe_write_creates_parent_dirs() {
1786        let temp = tempdir().unwrap();
1787        let file_path = temp.path().join("subdir").join("test.txt");
1788
1789        safe_write(&file_path, "test content").unwrap();
1790
1791        assert!(file_path.exists());
1792        let content = std::fs::read_to_string(&file_path).unwrap();
1793        assert_eq!(content, "test content");
1794    }
1795
1796    #[test]
1797    fn test_copy_dir() {
1798        let temp = tempdir().unwrap();
1799        let src = temp.path().join("src");
1800        let dst = temp.path().join("dst");
1801
1802        // Create source structure
1803        ensure_dir(&src).unwrap();
1804        ensure_dir(&src.join("subdir")).unwrap();
1805        std::fs::write(src.join("file1.txt"), "content1").unwrap();
1806        std::fs::write(src.join("subdir/file2.txt"), "content2").unwrap();
1807
1808        // Copy directory
1809        copy_dir(&src, &dst).unwrap();
1810
1811        // Verify copy
1812        assert!(dst.join("file1.txt").exists());
1813        assert!(dst.join("subdir/file2.txt").exists());
1814
1815        let content1 = std::fs::read_to_string(dst.join("file1.txt")).unwrap();
1816        assert_eq!(content1, "content1");
1817
1818        let content2 = std::fs::read_to_string(dst.join("subdir/file2.txt")).unwrap();
1819        assert_eq!(content2, "content2");
1820    }
1821
1822    #[test]
1823    fn test_remove_dir_all() {
1824        let temp = tempdir().unwrap();
1825        let dir = temp.path().join("to_remove");
1826
1827        ensure_dir(&dir).unwrap();
1828        std::fs::write(dir.join("file.txt"), "content").unwrap();
1829
1830        assert!(dir.exists());
1831        remove_dir_all(&dir).unwrap();
1832        assert!(!dir.exists());
1833    }
1834
1835    #[test]
1836    fn test_remove_dir_all_nonexistent() {
1837        let temp = tempdir().unwrap();
1838        let dir = temp.path().join("nonexistent");
1839
1840        // Should not error on non-existent directory
1841        remove_dir_all(&dir).unwrap();
1842    }
1843
1844    #[test]
1845    fn test_find_files() {
1846        let temp = tempdir().unwrap();
1847        let root = temp.path();
1848
1849        // Create test files
1850        std::fs::write(root.join("test.rs"), "").unwrap();
1851        std::fs::write(root.join("main.rs"), "").unwrap();
1852        ensure_dir(&root.join("src")).unwrap();
1853        std::fs::write(root.join("src/lib.rs"), "").unwrap();
1854        std::fs::write(root.join("src/test.txt"), "").unwrap();
1855
1856        let files = find_files(root, ".rs").unwrap();
1857        assert_eq!(files.len(), 3);
1858
1859        let files = find_files(root, "test").unwrap();
1860        assert_eq!(files.len(), 2);
1861    }
1862
1863    #[test]
1864    fn test_dir_size() {
1865        let temp = tempdir().unwrap();
1866        let dir = temp.path();
1867
1868        std::fs::write(dir.join("file1.txt"), "12345").unwrap();
1869        std::fs::write(dir.join("file2.txt"), "123456789").unwrap();
1870        ensure_dir(&dir.join("subdir")).unwrap();
1871        std::fs::write(dir.join("subdir/file3.txt"), "abc").unwrap();
1872
1873        let size = dir_size(dir).unwrap();
1874        assert_eq!(size, 17); // 5 + 9 + 3
1875    }
1876
1877    #[test]
1878    fn test_temp_dir() {
1879        let temp_dir = TempDir::new("test").unwrap();
1880        let path = temp_dir.path().to_path_buf();
1881
1882        assert!(path.exists());
1883        assert!(path.is_dir());
1884
1885        // Write a file to verify it's a real directory
1886        std::fs::write(path.join("test.txt"), "test").unwrap();
1887        assert!(path.join("test.txt").exists());
1888
1889        drop(temp_dir);
1890        // Directory should be cleaned up
1891        assert!(!path.exists());
1892    }
1893
1894    #[test]
1895    fn test_ensure_parent_dir() {
1896        let temp = tempdir().unwrap();
1897        let file_path = temp.path().join("parent").join("child").join("file.txt");
1898
1899        ensure_parent_dir(&file_path).unwrap();
1900        assert!(file_path.parent().unwrap().exists());
1901    }
1902
1903    #[test]
1904    fn test_ensure_dir_exists() {
1905        let temp = tempdir().unwrap();
1906        let test_dir = temp.path().join("test_dir_alias");
1907
1908        assert!(!test_dir.exists());
1909        ensure_dir_exists(&test_dir).unwrap();
1910        assert!(test_dir.exists());
1911    }
1912
1913    #[test]
1914    fn test_copy_dir_all() {
1915        let temp = tempdir().unwrap();
1916        let src = temp.path().join("src_alias");
1917        let dst = temp.path().join("dst_alias");
1918
1919        ensure_dir(&src).unwrap();
1920        std::fs::write(src.join("file.txt"), "content").unwrap();
1921
1922        copy_dir_all(&src, &dst).unwrap();
1923        assert!(dst.join("file.txt").exists());
1924    }
1925
1926    #[test]
1927    fn test_find_project_root() {
1928        let temp = tempdir().unwrap();
1929        let project = temp.path().join("project");
1930        let subdir = project.join("src").join("subdir");
1931
1932        ensure_dir(&subdir).unwrap();
1933        std::fs::write(project.join("agpm.toml"), "[sources]").unwrap();
1934
1935        let root = find_project_root(&subdir).unwrap();
1936        assert_eq!(root.canonicalize().unwrap(), project.canonicalize().unwrap());
1937    }
1938
1939    #[test]
1940    fn test_find_project_root_not_found() {
1941        let temp = tempdir().unwrap();
1942        let result = find_project_root(temp.path());
1943        assert!(result.is_err());
1944    }
1945
1946    #[test]
1947    fn test_get_global_config_path() {
1948        let config_path = get_global_config_path().unwrap();
1949        assert!(config_path.to_string_lossy().contains(".config"));
1950        assert!(config_path.to_string_lossy().contains("agpm"));
1951    }
1952
1953    #[test]
1954    fn test_calculate_checksum() {
1955        let temp = tempdir().unwrap();
1956        let file = temp.path().join("checksum_test.txt");
1957        std::fs::write(&file, "test content").unwrap();
1958
1959        let checksum = calculate_checksum(&file).unwrap();
1960        assert!(!checksum.is_empty());
1961        assert_eq!(checksum.len(), 64); // SHA256 produces 64 hex chars
1962    }
1963
1964    #[tokio::test]
1965    async fn test_calculate_checksums_parallel() {
1966        let temp = tempdir().unwrap();
1967        let file1 = temp.path().join("file1.txt");
1968        let file2 = temp.path().join("file2.txt");
1969
1970        std::fs::write(&file1, "content1").unwrap();
1971        std::fs::write(&file2, "content2").unwrap();
1972
1973        let paths = vec![file1.clone(), file2.clone()];
1974        let results = calculate_checksums_parallel(&paths).await.unwrap();
1975
1976        assert_eq!(results.len(), 2);
1977        assert_eq!(results[0].0, file1);
1978        assert_eq!(results[1].0, file2);
1979        assert!(!results[0].1.is_empty());
1980        assert!(!results[1].1.is_empty());
1981    }
1982
1983    #[tokio::test]
1984    async fn test_calculate_checksums_parallel_empty() {
1985        let results = calculate_checksums_parallel(&[]).await.unwrap();
1986        assert!(results.is_empty());
1987    }
1988
1989    #[tokio::test]
1990    async fn test_copy_files_parallel() {
1991        let temp = tempdir().unwrap();
1992        let src1 = temp.path().join("src1.txt");
1993        let src2 = temp.path().join("src2.txt");
1994        let dst1 = temp.path().join("dst").join("dst1.txt");
1995        let dst2 = temp.path().join("dst").join("dst2.txt");
1996
1997        std::fs::write(&src1, "content1").unwrap();
1998        std::fs::write(&src2, "content2").unwrap();
1999
2000        let pairs = vec![(src1.clone(), dst1.clone()), (src2.clone(), dst2.clone())];
2001        copy_files_parallel(&pairs).await.unwrap();
2002
2003        assert!(dst1.exists());
2004        assert!(dst2.exists());
2005        assert_eq!(std::fs::read_to_string(&dst1).unwrap(), "content1");
2006        assert_eq!(std::fs::read_to_string(&dst2).unwrap(), "content2");
2007    }
2008
2009    #[tokio::test]
2010    async fn test_atomic_write_multiple() {
2011        let temp = tempdir().unwrap();
2012        let file1 = temp.path().join("atomic1.txt");
2013        let file2 = temp.path().join("atomic2.txt");
2014
2015        let files =
2016            vec![(file1.clone(), b"content1".to_vec()), (file2.clone(), b"content2".to_vec())];
2017
2018        atomic_write_multiple(&files).await.unwrap();
2019
2020        assert!(file1.exists());
2021        assert!(file2.exists());
2022        assert_eq!(std::fs::read_to_string(&file1).unwrap(), "content1");
2023        assert_eq!(std::fs::read_to_string(&file2).unwrap(), "content2");
2024    }
2025
2026    #[tokio::test]
2027    async fn test_copy_dirs_parallel() {
2028        let temp = tempdir().unwrap();
2029        let src1 = temp.path().join("src1");
2030        let src2 = temp.path().join("src2");
2031        let dst1 = temp.path().join("dst1");
2032        let dst2 = temp.path().join("dst2");
2033
2034        ensure_dir(&src1).unwrap();
2035        ensure_dir(&src2).unwrap();
2036        std::fs::write(src1.join("file1.txt"), "content1").unwrap();
2037        std::fs::write(src2.join("file2.txt"), "content2").unwrap();
2038
2039        let pairs = vec![(src1.clone(), dst1.clone()), (src2.clone(), dst2.clone())];
2040        copy_dirs_parallel(&pairs).await.unwrap();
2041
2042        assert!(dst1.join("file1.txt").exists());
2043        assert!(dst2.join("file2.txt").exists());
2044    }
2045
2046    #[tokio::test]
2047    async fn test_read_files_parallel() {
2048        let temp = tempdir().unwrap();
2049        let file1 = temp.path().join("read1.txt");
2050        let file2 = temp.path().join("read2.txt");
2051
2052        std::fs::write(&file1, "content1").unwrap();
2053        std::fs::write(&file2, "content2").unwrap();
2054
2055        let paths = vec![file1.clone(), file2.clone()];
2056        let results = read_files_parallel(&paths).await.unwrap();
2057
2058        assert_eq!(results.len(), 2);
2059        assert_eq!(results[0].0, file1);
2060        assert_eq!(results[0].1, "content1");
2061        assert_eq!(results[1].0, file2);
2062        assert_eq!(results[1].1, "content2");
2063    }
2064
2065    #[test]
2066    fn test_ensure_dir_on_file() {
2067        let temp = tempdir().unwrap();
2068        let file_path = temp.path().join("file.txt");
2069        std::fs::write(&file_path, "content").unwrap();
2070
2071        let result = ensure_dir(&file_path);
2072        assert!(result.is_err());
2073    }
2074
2075    #[tokio::test]
2076    async fn test_parallel_operations_empty() {
2077        // Test parallel operations with empty inputs
2078        let result = calculate_checksums_parallel(&[]).await;
2079        assert!(result.is_ok());
2080        assert!(result.unwrap().is_empty());
2081
2082        let result = copy_files_parallel(&[]).await;
2083        assert!(result.is_ok());
2084
2085        let result = atomic_write_multiple(&[]).await;
2086        assert!(result.is_ok());
2087
2088        let result = copy_dirs_parallel(&[]).await;
2089        assert!(result.is_ok());
2090
2091        let result = read_files_parallel(&[]).await;
2092        assert!(result.is_ok());
2093        assert!(result.unwrap().is_empty());
2094    }
2095
2096    #[test]
2097    fn test_atomic_write_basic() {
2098        let temp = tempdir().unwrap();
2099        let file = temp.path().join("atomic.txt");
2100
2101        atomic_write(&file, b"test content").unwrap();
2102        assert_eq!(std::fs::read_to_string(&file).unwrap(), "test content");
2103    }
2104
2105    #[test]
2106    fn test_atomic_write_overwrites() {
2107        let temp = tempdir().unwrap();
2108        let file = temp.path().join("atomic.txt");
2109
2110        // Write initial content
2111        atomic_write(&file, b"initial").unwrap();
2112        assert_eq!(std::fs::read_to_string(&file).unwrap(), "initial");
2113
2114        // Overwrite
2115        atomic_write(&file, b"updated").unwrap();
2116        assert_eq!(std::fs::read_to_string(&file).unwrap(), "updated");
2117    }
2118
2119    #[test]
2120    fn test_atomic_write_creates_parent() {
2121        let temp = tempdir().unwrap();
2122        let file = temp.path().join("deep").join("nested").join("atomic.txt");
2123
2124        atomic_write(&file, b"nested content").unwrap();
2125        assert!(file.exists());
2126        assert_eq!(std::fs::read_to_string(&file).unwrap(), "nested content");
2127    }
2128
2129    #[test]
2130    fn test_safe_copy_file() {
2131        let temp = tempdir().unwrap();
2132        let src = temp.path().join("source.txt");
2133        let dst = temp.path().join("dest.txt");
2134
2135        std::fs::write(&src, "test content").unwrap();
2136        std::fs::copy(&src, &dst).unwrap();
2137
2138        assert_eq!(std::fs::read_to_string(&dst).unwrap(), "test content");
2139    }
2140
2141    #[test]
2142    fn test_copy_with_parent_creation() {
2143        let temp = tempdir().unwrap();
2144        let src = temp.path().join("source.txt");
2145        let dst = temp.path().join("subdir").join("dest.txt");
2146
2147        std::fs::write(&src, "test content").unwrap();
2148        ensure_parent_dir(&dst).unwrap();
2149        std::fs::copy(&src, &dst).unwrap();
2150
2151        assert!(dst.exists());
2152        assert_eq!(std::fs::read_to_string(&dst).unwrap(), "test content");
2153    }
2154
2155    #[test]
2156    fn test_copy_nonexistent_source() {
2157        let temp = tempdir().unwrap();
2158        let src = temp.path().join("nonexistent.txt");
2159        let dst = temp.path().join("dest.txt");
2160
2161        let result = std::fs::copy(&src, &dst);
2162        assert!(result.is_err());
2163    }
2164
2165    #[test]
2166    fn test_normalize_path_complex() {
2167        // Test various path normalization scenarios
2168        assert_eq!(normalize_path(Path::new("/")), PathBuf::from("/"));
2169        assert_eq!(normalize_path(Path::new("/foo/bar")), PathBuf::from("/foo/bar"));
2170        assert_eq!(normalize_path(Path::new("/foo/./bar")), PathBuf::from("/foo/bar"));
2171        assert_eq!(normalize_path(Path::new("/foo/../bar")), PathBuf::from("/bar"));
2172        assert_eq!(normalize_path(Path::new("/foo/bar/..")), PathBuf::from("/foo"));
2173        assert_eq!(normalize_path(Path::new("foo/./bar")), PathBuf::from("foo/bar"));
2174        assert_eq!(normalize_path(Path::new("./foo/bar")), PathBuf::from("foo/bar"));
2175    }
2176
2177    #[test]
2178    fn test_is_safe_path_edge_cases() {
2179        let base = Path::new("/home/user/project");
2180
2181        // Safe paths
2182        assert!(is_safe_path(base, Path::new("")));
2183        assert!(is_safe_path(base, Path::new(".")));
2184        assert!(is_safe_path(base, Path::new("./nested/./path")));
2185
2186        // Unsafe paths
2187        assert!(!is_safe_path(base, Path::new("..")));
2188        assert!(!is_safe_path(base, Path::new("../../etc")));
2189        assert!(!is_safe_path(base, Path::new("/absolute/path")));
2190
2191        // Windows-style paths (on Unix these are relative)
2192        if cfg!(windows) {
2193            assert!(!is_safe_path(base, Path::new("C:\\Windows")));
2194        }
2195    }
2196
2197    #[test]
2198    fn test_safe_write_readonly_parent() {
2199        // This test verifies behavior when parent dir is readonly
2200        // We skip it in CI as it requires special permissions
2201        if std::env::var("CI").is_ok() {
2202            return;
2203        }
2204
2205        let temp = tempdir().unwrap();
2206        let readonly_dir = temp.path().join("readonly");
2207        ensure_dir(&readonly_dir).unwrap();
2208
2209        // Make directory readonly (Unix-specific)
2210        #[cfg(unix)]
2211        {
2212            use std::os::unix::fs::PermissionsExt;
2213            let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
2214            perms.set_mode(0o555); // r-xr-xr-x
2215            std::fs::set_permissions(&readonly_dir, perms).unwrap();
2216
2217            let file = readonly_dir.join("test.txt");
2218            let result = safe_write(&file, "test");
2219            assert!(result.is_err());
2220
2221            // Restore permissions for cleanup
2222            let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
2223            perms.set_mode(0o755);
2224            std::fs::set_permissions(&readonly_dir, perms).unwrap();
2225        }
2226    }
2227
2228    #[test]
2229    #[cfg(unix)]
2230    fn test_remove_dir_all_symlink() {
2231        // Test that remove_dir_all doesn't follow symlinks
2232        let temp = tempdir().unwrap();
2233        let target = temp.path().join("target");
2234        let link = temp.path().join("link");
2235
2236        ensure_dir(&target).unwrap();
2237        std::fs::write(target.join("important.txt"), "data").unwrap();
2238
2239        std::os::unix::fs::symlink(&target, &link).unwrap();
2240        remove_dir_all(&link).unwrap();
2241
2242        // Target should still exist
2243        assert!(target.exists());
2244        assert!(target.join("important.txt").exists());
2245    }
2246
2247    #[test]
2248    fn test_find_files_with_patterns() {
2249        let temp = tempdir().unwrap();
2250        let root = temp.path();
2251
2252        // Create test files
2253        std::fs::write(root.join("README.md"), "").unwrap();
2254        std::fs::write(root.join("test.MD"), "").unwrap(); // Different case
2255        std::fs::write(root.join("file.txt"), "").unwrap();
2256        ensure_dir(&root.join("hidden")).unwrap();
2257        std::fs::write(root.join("hidden/.secret.md"), "").unwrap();
2258
2259        // Pattern matching
2260        let files = find_files(root, ".md").unwrap();
2261        assert_eq!(files.len(), 2); // README.md and .secret.md
2262
2263        let files = find_files(root, ".MD").unwrap();
2264        assert_eq!(files.len(), 1); // test.MD
2265
2266        // Substring matching
2267        let files = find_files(root, "test").unwrap();
2268        assert_eq!(files.len(), 1);
2269
2270        let files = find_files(root, "secret").unwrap();
2271        assert_eq!(files.len(), 1);
2272    }
2273
2274    #[test]
2275    fn test_dir_size_edge_cases() {
2276        let temp = tempdir().unwrap();
2277
2278        // Empty directory
2279        let empty_dir = temp.path().join("empty");
2280        ensure_dir(&empty_dir).unwrap();
2281        assert_eq!(dir_size(&empty_dir).unwrap(), 0);
2282
2283        // Non-existent directory
2284        let nonexistent = temp.path().join("nonexistent");
2285        let result = dir_size(&nonexistent);
2286        assert!(result.is_err());
2287
2288        // Directory with symlinks
2289        #[cfg(unix)]
2290        {
2291            let dir = temp.path().join("with_symlink");
2292            ensure_dir(&dir).unwrap();
2293            std::fs::write(dir.join("file.txt"), "12345").unwrap();
2294
2295            let target = temp.path().join("target");
2296            std::fs::write(&target, "123456789").unwrap();
2297            std::os::unix::fs::symlink(&target, dir.join("link")).unwrap();
2298
2299            // The dir_size function behavior with symlinks depends on the implementation
2300            // Just verify it doesn't crash and returns a reasonable size
2301            let size = dir_size(&dir).unwrap();
2302            // We should have at least the size of the real file
2303            assert!(size >= 5);
2304            // The size should be reasonable (not gigabytes)
2305            assert!(size < 1_000_000);
2306        }
2307    }
2308
2309    #[test]
2310    fn test_temp_dir_custom_prefix() {
2311        let temp1 = TempDir::new("prefix1").unwrap();
2312        let temp2 = TempDir::new("prefix2").unwrap();
2313
2314        assert!(temp1.path().to_string_lossy().contains("prefix1"));
2315        assert!(temp2.path().to_string_lossy().contains("prefix2"));
2316
2317        let path1 = temp1.path().to_path_buf();
2318        let path2 = temp2.path().to_path_buf();
2319
2320        assert_ne!(path1, path2);
2321        assert!(path1.exists());
2322        assert!(path2.exists());
2323    }
2324
2325    #[test]
2326    fn test_ensure_parent_dir_edge_cases() {
2327        let temp = tempdir().unwrap();
2328
2329        // File at root (no parent)
2330        let root_file = if cfg!(windows) {
2331            PathBuf::from("C:\\file.txt")
2332        } else {
2333            PathBuf::from("/file.txt")
2334        };
2335        ensure_parent_dir(&root_file).unwrap(); // Should not panic
2336
2337        // Current directory file
2338        let current_file = PathBuf::from("file.txt");
2339        ensure_parent_dir(&current_file).unwrap();
2340
2341        // Already existing parent
2342        let existing = temp.path().join("file.txt");
2343        ensure_parent_dir(&existing).unwrap();
2344        ensure_parent_dir(&existing).unwrap(); // Second call should be ok
2345    }
2346
2347    #[test]
2348    fn test_calculate_checksum_edge_cases() {
2349        let temp = tempdir().unwrap();
2350
2351        // Empty file
2352        let empty = temp.path().join("empty.txt");
2353        std::fs::write(&empty, "").unwrap();
2354        let checksum = calculate_checksum(&empty).unwrap();
2355        assert_eq!(checksum.len(), 64);
2356        // SHA256 of empty string is well-known
2357        assert_eq!(checksum, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
2358
2359        // Non-existent file
2360        let nonexistent = temp.path().join("nonexistent.txt");
2361        let result = calculate_checksum(&nonexistent);
2362        assert!(result.is_err());
2363
2364        // Large file (1MB)
2365        let large = temp.path().join("large.txt");
2366        let large_content = vec![b'a'; 1024 * 1024];
2367        std::fs::write(&large, &large_content).unwrap();
2368        let checksum = calculate_checksum(&large).unwrap();
2369        assert_eq!(checksum.len(), 64);
2370    }
2371
2372    #[tokio::test]
2373    async fn test_calculate_checksums_parallel_errors() {
2374        let temp = tempdir().unwrap();
2375        let valid = temp.path().join("valid.txt");
2376        let invalid = temp.path().join("invalid.txt");
2377
2378        std::fs::write(&valid, "content").unwrap();
2379
2380        let paths = vec![valid.clone(), invalid.clone()];
2381        let result = calculate_checksums_parallel(&paths).await;
2382
2383        // Should fail if any file is invalid
2384        assert!(result.is_err());
2385    }
2386
2387    #[tokio::test]
2388    async fn test_copy_files_parallel_errors() {
2389        let temp = tempdir().unwrap();
2390        let src = temp.path().join("nonexistent.txt");
2391        let dst = temp.path().join("dest.txt");
2392
2393        let pairs = vec![(src, dst)];
2394        let result = copy_files_parallel(&pairs).await;
2395
2396        assert!(result.is_err());
2397    }
2398
2399    #[tokio::test]
2400    async fn test_atomic_write_multiple_partial_failure() {
2401        // Test behavior when some writes might fail
2402        let temp = tempdir().unwrap();
2403        let valid_path = temp.path().join("valid.txt");
2404
2405        // Use an invalid path that will cause write to fail
2406        // Create a file and try to use it as a directory
2407        let invalid_base = temp.path().join("not_a_directory.txt");
2408        std::fs::write(&invalid_base, "this is a file").unwrap();
2409        let invalid_path = invalid_base.join("impossible_file.txt");
2410
2411        let files =
2412            vec![(valid_path.clone(), b"content".to_vec()), (invalid_path, b"fail".to_vec())];
2413
2414        let result = atomic_write_multiple(&files).await;
2415        assert!(result.is_err());
2416    }
2417
2418    #[tokio::test]
2419    async fn test_read_files_parallel_mixed() {
2420        let temp = tempdir().unwrap();
2421        let valid = temp.path().join("valid.txt");
2422        let invalid = temp.path().join("invalid.txt");
2423
2424        std::fs::write(&valid, "content").unwrap();
2425
2426        let paths = vec![valid, invalid];
2427        let result = read_files_parallel(&paths).await;
2428
2429        // Should fail if any file cannot be read
2430        assert!(result.is_err());
2431    }
2432
2433    #[tokio::test]
2434    async fn test_copy_dirs_parallel_errors() {
2435        let temp = tempdir().unwrap();
2436        let src = temp.path().join("nonexistent");
2437        let dst = temp.path().join("dest");
2438
2439        let pairs = vec![(src, dst)];
2440        let result = copy_dirs_parallel(&pairs).await;
2441
2442        assert!(result.is_err());
2443    }
2444
2445    #[test]
2446    fn test_copy_dir_with_permissions() {
2447        let temp = tempdir().unwrap();
2448        let src = temp.path().join("src");
2449        let dst = temp.path().join("dst");
2450
2451        ensure_dir(&src).unwrap();
2452        std::fs::write(src.join("file.txt"), "content").unwrap();
2453
2454        // Set specific permissions on Unix
2455        #[cfg(unix)]
2456        {
2457            use std::os::unix::fs::PermissionsExt;
2458            let mut perms = std::fs::metadata(src.join("file.txt")).unwrap().permissions();
2459            perms.set_mode(0o644);
2460            std::fs::set_permissions(src.join("file.txt"), perms).unwrap();
2461        }
2462
2463        copy_dir(&src, &dst).unwrap();
2464
2465        assert!(dst.join("file.txt").exists());
2466
2467        // Verify permissions were preserved on Unix
2468        #[cfg(unix)]
2469        {
2470            use std::os::unix::fs::PermissionsExt;
2471            let perms = std::fs::metadata(dst.join("file.txt")).unwrap().permissions();
2472            assert_eq!(perms.mode() & 0o777, 0o644);
2473        }
2474    }
2475
2476    #[test]
2477    fn test_find_project_root_multiple_markers() {
2478        let temp = tempdir().unwrap();
2479        let root = temp.path().join("project");
2480        let subproject = root.join("subproject");
2481        let deep = subproject.join("src");
2482
2483        ensure_dir(&deep).unwrap();
2484        std::fs::write(root.join("agpm.toml"), "[sources]").unwrap();
2485        std::fs::write(subproject.join("agpm.toml"), "[sources]").unwrap();
2486
2487        // Should find the closest agpm.toml
2488        let found = find_project_root(&deep).unwrap();
2489        assert_eq!(found.canonicalize().unwrap(), subproject.canonicalize().unwrap());
2490    }
2491
2492    #[test]
2493    fn test_get_cache_dir_from_config() {
2494        // Test that we can get a cache directory (using the config module)
2495        // Without setting env vars (to avoid race conditions in parallel tests)
2496        let cache_dir = crate::config::get_cache_dir().unwrap();
2497
2498        // The cache directory should be a valid non-empty path
2499        assert!(!cache_dir.as_os_str().is_empty());
2500
2501        // When no env var is set, it should contain "agpm" in its path
2502        // (unless AGPM_CACHE_DIR is already set in the test environment)
2503        if std::env::var("AGPM_CACHE_DIR").is_err() {
2504            assert!(cache_dir.to_string_lossy().contains("agpm"));
2505        }
2506    }
2507}