git_x/core/
validation.rs

1use crate::core::git::GitOperations;
2use crate::{GitXError, Result};
3
4/// Common validation utilities
5pub struct Validate;
6
7impl Validate {
8    /// Validate that a commit exists
9    pub fn commit_exists(commit: &str) -> Result<()> {
10        if GitOperations::commit_exists(commit)? {
11            Ok(())
12        } else {
13            Err(GitXError::GitCommand(format!(
14                "Commit '{commit}' does not exist"
15            )))
16        }
17    }
18
19    /// Validate that we're in a git repository
20    pub fn in_git_repo() -> Result<()> {
21        match GitOperations::repo_root() {
22            Ok(_) => Ok(()),
23            Err(_) => Err(GitXError::GitCommand("Not in a git repository".to_string())),
24        }
25    }
26
27    /// Validate branch name format
28    pub fn branch_name(name: &str) -> Result<()> {
29        if name.is_empty() {
30            return Err(GitXError::Parse("Branch name cannot be empty".to_string()));
31        }
32
33        // Check for invalid characters
34        let invalid_chars = [' ', '~', '^', ':', '?', '*', '[', '\\'];
35        if name.chars().any(|c| invalid_chars.contains(&c)) {
36            return Err(GitXError::Parse(format!(
37                "Branch name '{name}' contains invalid characters"
38            )));
39        }
40
41        // Check for reserved names
42        if name == "HEAD" || name.starts_with('-') {
43            return Err(GitXError::Parse(format!(
44                "Branch name '{name}' is reserved"
45            )));
46        }
47
48        Ok(())
49    }
50
51    /// Validate commit hash format (40 chars hex or 7+ chars for short hash)
52    pub fn commit_hash(hash: &str) -> Result<()> {
53        if hash.is_empty() {
54            return Err(GitXError::Parse("Commit hash cannot be empty".to_string()));
55        }
56
57        // Check length - must be at least 4 chars for short hash, max 40 for full
58        if hash.len() < 4 || hash.len() > 40 {
59            return Err(GitXError::Parse(format!(
60                "Invalid commit hash format: '{hash}' (must be 4-40 characters)"
61            )));
62        }
63
64        // Check if all characters are hexadecimal
65        if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
66            return Err(GitXError::Parse(format!(
67                "Invalid commit hash format: '{hash}' (must contain only hexadecimal characters)"
68            )));
69        }
70
71        Ok(())
72    }
73
74    /// Validate remote name format
75    pub fn remote_name(name: &str) -> Result<()> {
76        if name.is_empty() {
77            return Err(GitXError::Parse("Remote name cannot be empty".to_string()));
78        }
79
80        // Check for invalid characters
81        let invalid_chars = [
82            ' ', '\t', '\n', '\r', '/', '\\', ':', '?', '*', '[', ']', '^', '~',
83        ];
84        if name.chars().any(|c| invalid_chars.contains(&c)) {
85            return Err(GitXError::Parse(format!(
86                "Remote name '{name}' contains invalid characters"
87            )));
88        }
89
90        // Check for reserved names and patterns
91        if name.starts_with('-') || name.ends_with('-') || name.contains("..") {
92            return Err(GitXError::Parse(format!(
93                "Remote name '{name}' uses invalid pattern"
94            )));
95        }
96
97        Ok(())
98    }
99
100    /// Validate file path for git operations
101    pub fn file_path(path: &str) -> Result<()> {
102        if path.is_empty() {
103            return Err(GitXError::Parse("File path cannot be empty".to_string()));
104        }
105
106        // Check for dangerous paths
107        if path.contains("..") || path.starts_with('/') {
108            return Err(GitXError::Parse(format!(
109                "File path '{path}' is not allowed (no .. or absolute paths)"
110            )));
111        }
112
113        // Check for null bytes or other problematic characters
114        if path.contains('\0') || path.contains('\r') || path.contains('\n') {
115            return Err(GitXError::Parse(format!(
116                "File path '{path}' contains invalid characters"
117            )));
118        }
119
120        Ok(())
121    }
122
123    /// Validate that a number is within a reasonable range
124    pub fn positive_number(value: i32, max: Option<i32>, field_name: &str) -> Result<()> {
125        if value < 0 {
126            return Err(GitXError::Parse(format!(
127                "{field_name} must be positive, got {value}"
128            )));
129        }
130
131        if let Some(max_val) = max {
132            if value > max_val {
133                return Err(GitXError::Parse(format!(
134                    "{field_name} must be <= {max_val}, got {value}"
135                )));
136            }
137        }
138
139        Ok(())
140    }
141
142    /// Validate date/time string format for git operations
143    pub fn git_date_format(date_str: &str) -> Result<()> {
144        if date_str.is_empty() {
145            return Err(GitXError::Parse("Date string cannot be empty".to_string()));
146        }
147
148        // Check for obviously malicious input
149        if date_str.contains(';') || date_str.contains('&') || date_str.contains('|') {
150            return Err(GitXError::Parse(format!(
151                "Date string '{date_str}' contains invalid characters"
152            )));
153        }
154
155        // Basic length check to prevent excessively long input
156        if date_str.len() > 100 {
157            return Err(GitXError::Parse(format!(
158                "Date string too long: {}",
159                date_str.len()
160            )));
161        }
162
163        Ok(())
164    }
165
166    /// Validate string doesn't contain shell injection patterns
167    pub fn safe_string(input: &str, field_name: &str) -> Result<()> {
168        if input.is_empty() {
169            return Err(GitXError::Parse(format!("{field_name} cannot be empty")));
170        }
171
172        // Check for shell injection patterns
173        let dangerous_patterns = [
174            ";", "&", "|", "`", "$", "(", ")", "{", "}", "\\", "\n", "\r", " ",
175        ];
176        if dangerous_patterns
177            .iter()
178            .any(|&pattern| input.contains(pattern))
179        {
180            return Err(GitXError::Parse(format!(
181                "{field_name} contains potentially dangerous characters"
182            )));
183        }
184
185        // Basic length check
186        if input.len() > 1000 {
187            return Err(GitXError::Parse(format!(
188                "{field_name} is too long: {} characters",
189                input.len()
190            )));
191        }
192
193        Ok(())
194    }
195}
196
197/// Specific validators for different types of input
198pub struct BranchNameValidator;
199
200impl crate::core::traits::Validator<str> for BranchNameValidator {
201    fn validate(&self, input: &str) -> Result<()> {
202        Validate::branch_name(input)
203    }
204
205    fn validation_rules(&self) -> Vec<&'static str> {
206        vec![
207            "Cannot be empty",
208            "Cannot start with a dash",
209            "Cannot be 'HEAD'",
210            "Cannot contain spaces",
211            "Cannot contain ~^:?*[\\",
212        ]
213    }
214}
215
216pub struct CommitHashValidator;
217
218impl crate::core::traits::Validator<str> for CommitHashValidator {
219    fn validate(&self, input: &str) -> Result<()> {
220        Validate::commit_hash(input)
221    }
222
223    fn validation_rules(&self) -> Vec<&'static str> {
224        vec![
225            "Must be 4-40 characters long",
226            "Must contain only hex characters (0-9, a-f)",
227            "Must reference an existing commit",
228        ]
229    }
230}
231
232pub struct RemoteNameValidator;
233
234impl crate::core::traits::Validator<str> for RemoteNameValidator {
235    fn validate(&self, input: &str) -> Result<()> {
236        Validate::remote_name(input)
237    }
238
239    fn validation_rules(&self) -> Vec<&'static str> {
240        vec![
241            "Cannot be empty",
242            "Cannot contain special characters",
243            "Cannot start or end with dash",
244            "Cannot contain '..'",
245        ]
246    }
247}