Skip to content

Commit 3626661

Browse files
committed
fix(processor): ensure correct error on Linux
1 parent 4169478 commit 3626661

File tree

3 files changed

+98
-31
lines changed

3 files changed

+98
-31
lines changed

src/processor/action_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ pub(crate) fn process_single_action(
9393

9494
// --- Handle Errors from Action Handlers ---
9595
if let Err(e) = result {
96+
// *** DEBUG LOG ***
97+
eprintln!("[DEBUG] Action handler returned error variant: {:?}", e);
9698
eprintln!("Error processing action for '{}': {}", relative_path_str, e);
9799
summary_updater::update_summary_error(summary, e);
98100
}

src/processor/create.rs

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::core_types::{Action, CreateStatus};
44
use crate::errors::ProcessError;
55
use std::fs;
6+
use std::io; // Import io for ErrorKind
67
use std::path::Path;
78

89
/// Creates or overwrites a file with the provided content.
@@ -19,6 +20,7 @@ pub(crate) fn process_create(
1920
.ok_or_else(|| ProcessError::Internal("Missing content for create action".to_string()))?;
2021

2122
// Ensure parent directory exists and is a directory
23+
// This might return ParentIsNotDirectory if parent exists as file or if creation fails
2224
ensure_parent_directory(resolved_full_path, resolved_base)?;
2325

2426
let mut status = CreateStatus::Created; // Default optimistic status
@@ -54,7 +56,23 @@ pub(crate) fn process_create(
5456

5557
// Write the file content (as bytes to preserve line endings)
5658
fs::write(resolved_full_path, content.as_bytes())
57-
.map_err(|e| ProcessError::Io { source: e })?;
59+
.map_err(|e| {
60+
// Check if the write failed because the parent path component is a file
61+
if e.kind() == io::ErrorKind::NotADirectory {
62+
// Map this specific IO error to our more descriptive error
63+
let parent_path = resolved_full_path.parent().unwrap_or(resolved_full_path).to_path_buf();
64+
// *** DEBUG LOG ***
65+
eprintln!("[DEBUG] fs::write failed with NotADirectory, mapping to ParentIsNotDirectory for path: {}", resolved_full_path.display());
66+
ProcessError::ParentIsNotDirectory {
67+
path: resolved_full_path.to_path_buf(),
68+
parent_path, // Report the parent path
69+
}
70+
} else {
71+
// *** DEBUG LOG ***
72+
eprintln!("[DEBUG] fs::write failed with other IO error: {:?}, mapping to Io for path: {}", e.kind(), resolved_full_path.display());
73+
ProcessError::Io { source: e }
74+
}
75+
})?;
5876

5977
Ok(status)
6078
}
@@ -63,21 +81,59 @@ pub(crate) fn process_create(
6381
/// Also checks if the parent path itself is unexpectedly a file.
6482
fn ensure_parent_directory(target_path: &Path, resolved_base: &Path) -> Result<(), ProcessError> {
6583
if let Some(parent_dir) = target_path.parent() {
66-
if parent_dir == resolved_base || parent_dir.exists() {
67-
// If parent exists, ensure it's a directory
68-
if !parent_dir.is_dir() {
69-
return Err(ProcessError::ParentIsNotDirectory {
70-
path: target_path.to_path_buf(),
71-
parent_path: parent_dir.to_path_buf(),
72-
});
84+
// Avoid checking the base directory itself if it's the parent
85+
if parent_dir == resolved_base || parent_dir.as_os_str().is_empty() {
86+
return Ok(()); // Base directory is guaranteed to exist and be a dir, or path is in root
87+
}
88+
89+
match fs::metadata(parent_dir) {
90+
Ok(metadata) => {
91+
// Parent exists, check if it's a directory
92+
if !metadata.is_dir() {
93+
// *** DEBUG LOG ***
94+
eprintln!("[DEBUG] Parent metadata exists but is not dir, returning ParentIsNotDirectory for parent: {}", parent_dir.display());
95+
return Err(ProcessError::ParentIsNotDirectory {
96+
path: target_path.to_path_buf(),
97+
parent_path: parent_dir.to_path_buf(),
98+
});
99+
}
100+
// Parent exists and is a directory, all good.
101+
}
102+
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
103+
// Parent does not exist, try to create it
104+
let relative_parent_dir =
105+
parent_dir.strip_prefix(resolved_base).unwrap_or(parent_dir);
106+
println!(" Creating directory: {}", relative_parent_dir.display());
107+
108+
if let Err(create_err) = fs::create_dir_all(parent_dir) {
109+
// Check if the error is specifically "Not a directory"
110+
// This often indicates an intermediate path component was a file during creation attempt.
111+
if create_err.kind() == io::ErrorKind::NotADirectory {
112+
// *** DEBUG LOG ***
113+
eprintln!("[DEBUG] create_dir_all failed with NotADirectory, returning ParentIsNotDirectory for parent: {}", parent_dir.display());
114+
// Map this specific IO error to our more descriptive error
115+
return Err(ProcessError::ParentIsNotDirectory {
116+
path: target_path.to_path_buf(),
117+
// Report the parent directory we *failed* to create
118+
parent_path: parent_dir.to_path_buf(),
119+
});
120+
} else {
121+
// *** DEBUG LOG ***
122+
eprintln!("[DEBUG] create_dir_all failed with other IO error: {:?}, returning Io for parent: {}", create_err.kind(), parent_dir.display());
123+
// Other I/O error during creation
124+
return Err(ProcessError::Io { source: create_err });
125+
}
126+
}
127+
// Creation successful
128+
}
129+
Err(e) => {
130+
// *** DEBUG LOG ***
131+
eprintln!("[DEBUG] fs::metadata failed with other IO error: {:?}, returning Io for parent: {}", e.kind(), parent_dir.display());
132+
// Other error getting metadata (permissions?)
133+
return Err(ProcessError::Io { source: e });
73134
}
74-
} else {
75-
// Parent does not exist, create it
76-
let relative_parent_dir = parent_dir.strip_prefix(resolved_base).unwrap_or(parent_dir);
77-
println!(" Creating directory: {}", relative_parent_dir.display());
78-
fs::create_dir_all(parent_dir).map_err(|e| ProcessError::Io { source: e })?;
79135
}
80136
}
81-
// If no parent (e.g., root directory), assume it's okay.
137+
// If no parent (e.g., file directly in base), assume it's okay.
82138
Ok(())
83139
}

src/processor/safety.rs

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,49 @@
11
//! Path safety validation logic.
22
33
use crate::errors::ProcessError;
4+
use std::fs; // Use fs::metadata
45
use std::io::ErrorKind;
56
use std::path::Path;
67

78
/// Checks if the target path is safely within the base directory.
8-
/// Canonicalizes both paths for reliable comparison.
9+
/// Canonicalizes paths for reliable comparison.
910
pub(crate) fn ensure_path_safe(base_dir: &Path, target_path: &Path) -> Result<(), ProcessError> {
1011
// Canonicalize base directory (must succeed as it's resolved in process_actions)
1112
let canonical_base = base_dir
1213
.canonicalize()
1314
.map_err(|e| ProcessError::Io { source: e })?;
1415

15-
// Attempt to canonicalize the target path.
16-
match target_path.canonicalize() {
17-
Ok(canonical_target) => {
18-
// If target exists and canonicalizes, check if it starts with the base
19-
if canonical_target.starts_with(&canonical_base) {
20-
Ok(()) // Path is safe
21-
} else {
22-
Err(ProcessError::PathNotSafe {
23-
resolved_path: canonical_target,
24-
base_path: canonical_base,
25-
})
16+
// Check if the target path *exists* first using metadata.
17+
match fs::metadata(target_path) {
18+
Ok(_) => {
19+
// Target exists. Canonicalize it for the safety check.
20+
match target_path.canonicalize() {
21+
Ok(canonical_target) => {
22+
if canonical_target.starts_with(&canonical_base) {
23+
Ok(()) // Path exists and is safe
24+
} else {
25+
Err(ProcessError::PathNotSafe {
26+
resolved_path: canonical_target,
27+
base_path: canonical_base,
28+
})
29+
}
30+
}
31+
Err(e) => {
32+
// Error canonicalizing an *existing* path (permissions?)
33+
Err(ProcessError::PathResolution {
34+
path: target_path.to_path_buf(),
35+
details: format!("Failed to canonicalize existing target path: {}", e),
36+
})
37+
}
2638
}
2739
}
2840
Err(ref e) if e.kind() == ErrorKind::NotFound => {
2941
// Target doesn't exist: Check safety based on its intended parent.
3042
check_nonexistent_target_safety(target_path, &canonical_base)
3143
}
3244
Err(e) => {
33-
// Other error during canonicalization (e.g., permission denied)
34-
Err(ProcessError::PathResolution {
35-
path: target_path.to_path_buf(),
36-
details: format!("Failed to canonicalize target path: {}", e),
37-
})
45+
// Other error getting metadata (permissions?)
46+
Err(ProcessError::Io { source: e }) // Map other metadata errors to IO
3847
}
3948
}
4049
}

0 commit comments

Comments
 (0)