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