exarch_core/security/
boundary.rs1use crate::error::ArchiveError;
13use crate::error::Result;
14
15pub const MAX_PATH_LENGTH: usize = 4096;
22
23pub fn validate_raw_path_str(path: &str) -> Result<()> {
63 if path.len() > MAX_PATH_LENGTH {
64 return Err(ArchiveError::violation(format!(
65 "path exceeds maximum length of {MAX_PATH_LENGTH} bytes (got {} bytes)",
66 path.len()
67 )));
68 }
69
70 if path.contains('\0') {
71 return Err(ArchiveError::violation(
72 "path contains null bytes - potential security issue".to_string(),
73 ));
74 }
75
76 Ok(())
77}
78
79pub const MAX_CONFIG_ENTRY_LENGTH: usize = 255;
82
83pub fn validate_config_entry(value: &str, field: &str) -> Result<()> {
119 if value.len() > MAX_CONFIG_ENTRY_LENGTH {
120 return Err(ArchiveError::InvalidConfiguration {
121 reason: format!(
122 "{field} exceeds maximum length of {MAX_CONFIG_ENTRY_LENGTH} bytes (got {} bytes)",
123 value.len()
124 ),
125 });
126 }
127
128 if value.contains('\0') {
129 return Err(ArchiveError::InvalidConfiguration {
130 reason: format!("{field} contains null bytes - potential security issue"),
131 });
132 }
133
134 if value.is_empty() {
135 return Err(ArchiveError::InvalidConfiguration {
136 reason: format!("{field} must not be empty"),
137 });
138 }
139
140 Ok(())
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used, clippy::expect_used)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_validate_raw_path_str_accepts_normal() {
150 assert!(validate_raw_path_str("/tmp/test.tar.gz").is_ok());
151 assert!(validate_raw_path_str("relative/path.tar").is_ok());
152 assert!(validate_raw_path_str("").is_ok());
153 }
154
155 #[test]
156 fn test_validate_raw_path_str_rejects_null_bytes() {
157 let err = validate_raw_path_str("/tmp/test\0malicious").expect_err("null byte");
158 match err {
159 ArchiveError::SecurityViolation { reason, .. } => {
160 assert_eq!(
161 reason,
162 "path contains null bytes - potential security issue"
163 );
164 }
165 other => panic!("expected SecurityViolation, got: {other:?}"),
166 }
167 }
168
169 #[test]
170 fn test_validate_raw_path_str_rejects_too_long() {
171 let long_path = "x".repeat(MAX_PATH_LENGTH + 1);
172 let err = validate_raw_path_str(&long_path).expect_err("too long");
173 match err {
174 ArchiveError::SecurityViolation { reason, .. } => {
175 assert_eq!(
176 reason,
177 format!(
178 "path exceeds maximum length of {MAX_PATH_LENGTH} bytes (got {} bytes)",
179 long_path.len()
180 )
181 );
182 }
183 other => panic!("expected SecurityViolation, got: {other:?}"),
184 }
185 }
186
187 #[test]
188 fn test_validate_raw_path_str_checks_length_before_null_byte() {
189 let long_path_with_null = format!("{}\0", "x".repeat(MAX_PATH_LENGTH));
190 let err = validate_raw_path_str(&long_path_with_null).expect_err("too long");
191 match err {
192 ArchiveError::SecurityViolation { reason, .. } => {
193 assert!(
194 reason.contains("maximum length"),
195 "length check should run first: {reason}"
196 );
197 }
198 other => panic!("expected SecurityViolation, got: {other:?}"),
199 }
200 }
201
202 #[test]
203 fn test_validate_raw_path_str_accepts_max_length() {
204 let max_path = "x".repeat(MAX_PATH_LENGTH);
205 assert!(validate_raw_path_str(&max_path).is_ok());
206 }
207
208 #[test]
209 fn test_validate_config_entry_accepts_normal() {
210 assert!(validate_config_entry("txt", "extension").is_ok());
211 assert!(validate_config_entry(".git", "banned path component").is_ok());
212 }
213
214 #[test]
215 fn test_validate_config_entry_rejects_empty() {
216 let err = validate_config_entry("", "extension").expect_err("empty");
217 match err {
218 ArchiveError::InvalidConfiguration { reason } => {
219 assert_eq!(reason, "extension must not be empty");
220 }
221 other => panic!("expected InvalidConfiguration, got: {other:?}"),
222 }
223 }
224
225 #[test]
226 fn test_validate_config_entry_rejects_null_bytes() {
227 let err = validate_config_entry("bad\0ext", "extension").expect_err("null byte");
228 match err {
229 ArchiveError::InvalidConfiguration { reason } => {
230 assert_eq!(
231 reason,
232 "extension contains null bytes - potential security issue"
233 );
234 }
235 other => panic!("expected InvalidConfiguration, got: {other:?}"),
236 }
237 }
238
239 #[test]
240 fn test_validate_config_entry_accepts_max_length() {
241 let max_entry = "x".repeat(MAX_CONFIG_ENTRY_LENGTH);
242 assert!(validate_config_entry(&max_entry, "extension").is_ok());
243 }
244
245 #[test]
246 fn test_validate_config_entry_rejects_too_long() {
247 let long_entry = "x".repeat(MAX_CONFIG_ENTRY_LENGTH + 1);
248 let err = validate_config_entry(&long_entry, "extension").expect_err("too long");
249 match err {
250 ArchiveError::InvalidConfiguration { reason } => {
251 assert_eq!(
252 reason,
253 format!(
254 "extension exceeds maximum length of {MAX_CONFIG_ENTRY_LENGTH} bytes (got {} bytes)",
255 long_entry.len()
256 )
257 );
258 }
259 other => panic!("expected InvalidConfiguration, got: {other:?}"),
260 }
261 }
262
263 #[test]
264 fn test_validate_config_entry_rejects_multibyte_over_length() {
265 let multibyte_entry = "日".repeat(100);
268 assert!(multibyte_entry.chars().count() < MAX_CONFIG_ENTRY_LENGTH);
269 let err =
270 validate_config_entry(&multibyte_entry, "extension").expect_err("too long in bytes");
271 match err {
272 ArchiveError::InvalidConfiguration { reason } => {
273 assert!(
274 reason.contains("bytes"),
275 "message must use byte-length wording: {reason}"
276 );
277 }
278 other => panic!("expected InvalidConfiguration, got: {other:?}"),
279 }
280 }
281
282 #[test]
283 fn test_validate_config_entry_checks_length_before_null_byte() {
284 let long_entry_with_null = format!("{}\0", "x".repeat(MAX_CONFIG_ENTRY_LENGTH));
285 let err = validate_config_entry(&long_entry_with_null, "extension").expect_err("too long");
286 match err {
287 ArchiveError::InvalidConfiguration { reason } => {
288 assert!(
289 reason.contains("maximum length"),
290 "length check should run first: {reason}"
291 );
292 }
293 other => panic!("expected InvalidConfiguration, got: {other:?}"),
294 }
295 }
296}