Skip to main content

xchecker_engine/fixup/
mod.rs

1//! Fixup detection and parsing system with preview/apply modes
2//!
3//! This module implements the fixup system that detects "FIXUP PLAN:" markers in review output,
4//! parses unified diff blocks, and provides preview/apply modes for safe fixup application.
5//!
6//! # Security
7//!
8//! All path operations use the sandboxed path types from `crate::paths` to prevent:
9//! - Directory traversal attacks via `..` components
10//! - Absolute path escapes
11//! - Symlink-based escapes (configurable)
12//! - Hardlink-based escapes (configurable)
13//!
14//! The `FixupParser` uses `SandboxRoot` to validate all target paths before any file operations.
15//! This ensures that diff application cannot escape the workspace root.
16
17mod apply;
18mod match_context;
19mod model;
20mod parse;
21mod paths;
22mod phase;
23mod report;
24
25pub use crate::error::FixupError;
26pub use crate::gate::{PendingFixupsResult, PendingFixupsStats};
27pub use apply::normalize_line_endings_for_diff;
28pub use model::{
29    AppliedFile, ChangeSummary, DiffHunk, FixupMode, FixupPreview, FixupResult, UnifiedDiff,
30};
31pub use parse::FixupParser;
32pub use paths::validate_fixup_target;
33pub use phase::FixupPhase;
34pub use report::{pending_fixups_for_spec, pending_fixups_result_from_handle};
35
36#[cfg(test)]
37mod tests {
38    use super::FixupError;
39    use crate::error::UserFriendlyError;
40    use std::path::PathBuf;
41
42    #[test]
43    fn test_fixup_error_user_friendly() {
44        let no_markers_err = FixupError::NoFixupMarkersFound;
45        assert!(!no_markers_err.user_message().is_empty());
46        assert!(no_markers_err.context().is_some());
47        assert!(!no_markers_err.suggestions().is_empty());
48
49        let invalid_diff_err = FixupError::InvalidDiffFormat {
50            block_index: 1,
51            reason: "missing hunk header".to_string(),
52        };
53        assert!(invalid_diff_err.user_message().contains("block 1"));
54        assert!(invalid_diff_err.context().is_some());
55        assert!(!invalid_diff_err.suggestions().is_empty());
56
57        let symlink_err = FixupError::SymlinkNotAllowed(PathBuf::from("test/file.txt"));
58        assert!(symlink_err.user_message().contains("--allow-links"));
59        assert!(symlink_err.context().is_some());
60        let suggestions = symlink_err.suggestions();
61        assert!(suggestions.iter().any(|s| s.contains("--allow-links")));
62
63        let abs_path_err = FixupError::AbsolutePath(PathBuf::from("/absolute/path"));
64        assert!(abs_path_err.user_message().contains("Absolute paths"));
65        assert!(abs_path_err.context().is_some());
66        assert!(!abs_path_err.suggestions().is_empty());
67    }
68}