aprender_shell/
validation.rs1use crate::error::ShellError;
7use crate::model::MarkovModel;
8use std::path::Path;
9
10pub fn sanitize_prefix(input: &str) -> Result<String, ShellError> {
30 let sanitized = input.replace('\0', "");
32
33 let trimmed = sanitized.trim();
35
36 if trimmed.is_empty() {
38 return Err(ShellError::InvalidInput {
39 message: "Empty prefix".into(),
40 });
41 }
42
43 if trimmed.len() < 2 {
45 return Err(ShellError::InvalidInput {
46 message: "Prefix too short (minimum 2 characters)".into(),
47 });
48 }
49
50 if trimmed.chars().any(|c| c.is_control() && c != '\t') {
52 return Err(ShellError::InvalidInput {
53 message: "Invalid control characters in input".into(),
54 });
55 }
56
57 Ok(trimmed.to_string())
58}
59
60pub fn load_model_graceful(path: &Path) -> Result<MarkovModel, ShellError> {
72 if !path.exists() {
74 return Err(ShellError::ModelNotFound {
75 path: path.to_path_buf(),
76 hint: "Run 'aprender-shell train' to create a model".into(),
77 });
78 }
79
80 match MarkovModel::load(path) {
82 Ok(model) => Ok(model),
83 Err(e) => {
84 let msg = e.to_string();
85
86 if msg.contains("Checksum")
88 || msg.contains("checksum")
89 || msg.contains("invalid")
90 || msg.contains("corrupt")
91 {
92 Err(ShellError::ModelCorrupted {
93 path: path.to_path_buf(),
94 hint: "Model file is corrupted. Run 'aprender-shell train' to rebuild".into(),
95 })
96 } else {
97 Err(ShellError::ModelLoadFailed {
98 path: path.to_path_buf(),
99 cause: msg,
100 })
101 }
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use std::io::Write;
110 use tempfile::NamedTempFile;
111
112 #[test]
117 fn test_empty_input_rejected() {
118 assert!(sanitize_prefix("").is_err());
119 let err = sanitize_prefix("").unwrap_err();
120 assert!(matches!(err, ShellError::InvalidInput { .. }));
121 }
122
123 #[test]
124 fn test_whitespace_only_rejected() {
125 assert!(sanitize_prefix(" ").is_err());
126 assert!(sanitize_prefix("\t\n").is_err());
127 assert!(sanitize_prefix(" \t ").is_err());
128 }
129
130 #[test]
131 fn test_null_bytes_removed() {
132 let result = sanitize_prefix("git \0status").expect("should succeed");
133 assert_eq!(result, "git status");
134 assert!(!result.contains('\0'));
135 }
136
137 #[test]
138 fn test_short_input_rejected() {
139 assert!(sanitize_prefix("g").is_err());
140 let err = sanitize_prefix("a").unwrap_err();
141 assert!(matches!(err, ShellError::InvalidInput { .. }));
142 }
143
144 #[test]
145 fn test_two_char_input_accepted() {
146 assert!(sanitize_prefix("gi").is_ok());
147 assert!(sanitize_prefix("ls").is_ok());
148 assert!(sanitize_prefix("cd").is_ok());
149 }
150
151 #[test]
152 fn test_double_dash_accepted() {
153 assert!(sanitize_prefix("--help").is_ok());
154 assert!(sanitize_prefix("-- filename").is_ok());
155 assert!(sanitize_prefix("git checkout --").is_ok());
156 }
157
158 #[test]
159 fn test_control_chars_rejected() {
160 assert!(sanitize_prefix("git\x07status").is_err());
162 assert!(sanitize_prefix("git\x1bstatus").is_err());
164 }
165
166 #[test]
167 fn test_tab_allowed() {
168 assert!(sanitize_prefix("git\tstatus").is_ok());
170 }
171
172 #[test]
173 fn test_valid_commands_accepted() {
174 assert!(sanitize_prefix("git status").is_ok());
175 assert!(sanitize_prefix("cargo build --release").is_ok());
176 assert!(sanitize_prefix("docker ps -a").is_ok());
177 assert!(sanitize_prefix("kubectl get pods").is_ok());
178 }
179
180 #[test]
181 fn test_whitespace_trimmed() {
182 let result = sanitize_prefix(" git status ").expect("should succeed");
183 assert_eq!(result, "git status");
184 }
185
186 #[test]
191 fn test_missing_model_graceful_error() {
192 let result = load_model_graceful(Path::new("/nonexistent/path/model.bin"));
193 assert!(matches!(result, Err(ShellError::ModelNotFound { .. })));
194
195 if let Err(ShellError::ModelNotFound { hint, .. }) = result {
196 assert!(hint.contains("train"));
197 }
198 }
199
200 #[test]
201 fn test_corrupted_model_graceful_error() {
202 let mut tmp = NamedTempFile::new().expect("create temp file");
204 tmp.write_all(b"GARBAGE DATA NOT A MODEL")
205 .expect("write garbage");
206 tmp.flush().expect("flush");
207
208 let result = load_model_graceful(tmp.path());
209
210 assert!(
212 matches!(
213 result,
214 Err(ShellError::ModelCorrupted { .. }) | Err(ShellError::ModelLoadFailed { .. })
215 ),
216 "Expected graceful error for corrupted model"
217 );
218 }
219
220 #[test]
221 fn test_valid_model_loads_successfully() {
222 let mut model = MarkovModel::new(3);
224 model.train(&["git status".to_string(), "git commit".to_string()]);
225
226 let tmp = NamedTempFile::new().expect("create temp file");
227 model.save(tmp.path()).expect("save model");
228
229 let result = load_model_graceful(tmp.path());
230 assert!(result.is_ok(), "Valid model should load successfully");
231 }
232
233 #[test]
234 fn test_empty_file_graceful_error() {
235 let tmp = NamedTempFile::new().expect("create temp file");
237
238 let result = load_model_graceful(tmp.path());
239
240 assert!(result.is_err(), "Empty file should fail gracefully");
242 }
243}