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