safeuploads 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,468 @@
1
+ """File security exception classes and error codes."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ # ============================================================================
7
+ # Configuration Validation
8
+ # ============================================================================
9
+
10
+
11
+ @dataclass
12
+ class ConfigValidationError:
13
+ """
14
+ Configuration validation issue with severity and recommendation.
15
+
16
+ Attributes:
17
+ error_type: Type of the validation error.
18
+ message: Human-readable error message.
19
+ severity: Error severity level ('error', 'warning', 'info').
20
+ component: Component that failed validation.
21
+ recommendation: Optional recommendation to fix the issue.
22
+ """
23
+
24
+ error_type: str
25
+ message: str
26
+ severity: str # 'error', 'warning', 'info'
27
+ component: str
28
+ recommendation: str = ""
29
+
30
+
31
+ class FileSecurityConfigurationError(Exception):
32
+ """
33
+ Configuration validation failed with aggregated errors.
34
+
35
+ Args:
36
+ errors: List of ConfigValidationError instances.
37
+
38
+ Attributes:
39
+ errors: List of validation errors that caused failure.
40
+ """
41
+
42
+ def __init__(self, errors: list[ConfigValidationError]):
43
+ self.errors = errors
44
+ error_messages = [
45
+ f"{error.severity.upper()}: {error.message}" for error in errors
46
+ ]
47
+ super().__init__(
48
+ f"Configuration validation failed: {'; '.join(error_messages)}"
49
+ )
50
+
51
+
52
+ # ============================================================================
53
+ # Error Codes
54
+ # ============================================================================
55
+
56
+
57
+ class ErrorCode:
58
+ """
59
+ Machine-readable error codes for file validation failures.
60
+
61
+ Attributes:
62
+ Error codes are class-level string constants for various
63
+ validation failure types.
64
+ """
65
+
66
+ # Filename validation errors
67
+ FILENAME_EMPTY = "FILENAME_EMPTY"
68
+ FILENAME_INVALID = "FILENAME_INVALID"
69
+ FILENAME_TOO_LONG = "FILENAME_TOO_LONG"
70
+
71
+ # Unicode security errors
72
+ UNICODE_SECURITY = "UNICODE_SECURITY"
73
+ UNICODE_DANGEROUS_CHARS = "UNICODE_DANGEROUS_CHARS"
74
+ UNICODE_NORMALIZATION_ERROR = "UNICODE_NORMALIZATION_ERROR"
75
+
76
+ # Extension validation errors
77
+ EXTENSION_BLOCKED = "EXTENSION_BLOCKED"
78
+ EXTENSION_NOT_ALLOWED = "EXTENSION_NOT_ALLOWED"
79
+ COMPOUND_EXTENSION_BLOCKED = "COMPOUND_EXTENSION_BLOCKED"
80
+ EXTENSION_MISSING = "EXTENSION_MISSING"
81
+
82
+ # Windows security errors
83
+ WINDOWS_RESERVED_NAME = "WINDOWS_RESERVED_NAME"
84
+
85
+ # File size errors
86
+ FILE_TOO_LARGE = "FILE_TOO_LARGE"
87
+ FILE_EMPTY = "FILE_EMPTY"
88
+ FILE_SIZE_UNKNOWN = "FILE_SIZE_UNKNOWN"
89
+
90
+ # MIME type errors
91
+ MIME_TYPE_INVALID = "MIME_TYPE_INVALID"
92
+ MIME_TYPE_MISMATCH = "MIME_TYPE_MISMATCH"
93
+ MIME_DETECTION_FAILED = "MIME_DETECTION_FAILED"
94
+
95
+ # File signature errors
96
+ FILE_SIGNATURE_INVALID = "FILE_SIGNATURE_INVALID"
97
+ FILE_SIGNATURE_MISSING = "FILE_SIGNATURE_MISSING"
98
+ FILE_SIGNATURE_MISMATCH = "FILE_SIGNATURE_MISMATCH"
99
+
100
+ # Compression and ZIP errors
101
+ ZIP_BOMB_DETECTED = "ZIP_BOMB_DETECTED"
102
+ ZIP_CONTENT_THREAT = "ZIP_CONTENT_THREAT"
103
+ COMPRESSION_RATIO_EXCEEDED = "COMPRESSION_RATIO_EXCEEDED"
104
+ ZIP_TOO_MANY_ENTRIES = "ZIP_TOO_MANY_ENTRIES"
105
+ ZIP_INVALID_STRUCTURE = "ZIP_INVALID_STRUCTURE"
106
+ ZIP_CORRUPT = "ZIP_CORRUPT"
107
+ ZIP_TOO_LARGE = "ZIP_TOO_LARGE"
108
+ ZIP_NESTED_ARCHIVE = "ZIP_NESTED_ARCHIVE"
109
+ ZIP_DIRECTORY_TRAVERSAL = "ZIP_DIRECTORY_TRAVERSAL"
110
+ ZIP_SYMLINK_DETECTED = "ZIP_SYMLINK_DETECTED"
111
+ ZIP_ABSOLUTE_PATH = "ZIP_ABSOLUTE_PATH"
112
+ ZIP_ANALYSIS_TIMEOUT = "ZIP_ANALYSIS_TIMEOUT"
113
+
114
+ # Processing errors
115
+ PROCESSING_ERROR = "PROCESSING_ERROR"
116
+ IO_ERROR = "IO_ERROR"
117
+ MEMORY_ERROR = "MEMORY_ERROR"
118
+
119
+
120
+ # ============================================================================
121
+ # Base Exceptions
122
+ # ============================================================================
123
+
124
+
125
+ class FileSecurityError(Exception):
126
+ """
127
+ Base exception for all file security validation failures.
128
+
129
+ Args:
130
+ message: Human-readable error description.
131
+ error_code: Optional machine-readable error code.
132
+
133
+ Attributes:
134
+ message: Human-readable error message.
135
+ error_code: Machine-readable error code from ErrorCode.
136
+ """
137
+
138
+ def __init__(self, message: str, error_code: str | None = None):
139
+ self.message = message
140
+ self.error_code = error_code
141
+ super().__init__(message)
142
+
143
+
144
+ # ============================================================================
145
+ # File Validation Exceptions
146
+ # ============================================================================
147
+
148
+
149
+ class FileValidationError(FileSecurityError):
150
+ """
151
+ File validation failed.
152
+
153
+ Args:
154
+ message: Human-readable error description.
155
+ filename: Optional name of the file that failed validation.
156
+ error_code: Optional machine-readable error code.
157
+
158
+ Attributes:
159
+ filename: Name of the file that failed validation.
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ message: str,
165
+ filename: str | None = None,
166
+ error_code: str | None = None,
167
+ ):
168
+ self.filename = filename
169
+ super().__init__(message, error_code)
170
+
171
+
172
+ # ============================================================================
173
+ # Filename Security Exceptions
174
+ # ============================================================================
175
+
176
+
177
+ class FilenameSecurityError(FileValidationError):
178
+ """Filename failed security checks."""
179
+
180
+ pass
181
+
182
+
183
+ class UnicodeSecurityError(FilenameSecurityError):
184
+ """
185
+ Dangerous Unicode characters detected in filename.
186
+
187
+ Args:
188
+ message: Human-readable error description.
189
+ filename: Optional filename containing dangerous Unicode.
190
+ dangerous_chars: Optional list of (char, code_point, position)
191
+ tuples for each dangerous character found.
192
+
193
+ Attributes:
194
+ dangerous_chars: List of dangerous character tuples.
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ message: str,
200
+ filename: str | None = None,
201
+ dangerous_chars: list[tuple[str, int, int]] | None = None,
202
+ ):
203
+ self.dangerous_chars = dangerous_chars or []
204
+ super().__init__(
205
+ message,
206
+ filename=filename,
207
+ error_code=ErrorCode.UNICODE_DANGEROUS_CHARS,
208
+ )
209
+
210
+
211
+ class ExtensionSecurityError(FilenameSecurityError):
212
+ """
213
+ Dangerous file extension detected.
214
+
215
+ Args:
216
+ message: Human-readable error description.
217
+ filename: Optional filename with dangerous extension.
218
+ extension: Optional specific extension that was blocked.
219
+ error_code: Optional error code (defaults to
220
+ EXTENSION_BLOCKED).
221
+
222
+ Attributes:
223
+ extension: The specific extension that was blocked.
224
+ """
225
+
226
+ def __init__(
227
+ self,
228
+ message: str,
229
+ filename: str | None = None,
230
+ extension: str | None = None,
231
+ error_code: str | None = None,
232
+ ):
233
+ self.extension = extension
234
+ super().__init__(
235
+ message,
236
+ filename=filename,
237
+ error_code=error_code or ErrorCode.EXTENSION_BLOCKED,
238
+ )
239
+
240
+
241
+ class WindowsReservedNameError(FilenameSecurityError):
242
+ """
243
+ Windows reserved device name used.
244
+
245
+ Args:
246
+ message: Human-readable error description.
247
+ filename: Optional filename using a reserved name.
248
+ reserved_name: Optional specific reserved name detected.
249
+
250
+ Attributes:
251
+ reserved_name: The specific reserved name that was detected.
252
+ """
253
+
254
+ def __init__(
255
+ self,
256
+ message: str,
257
+ filename: str | None = None,
258
+ reserved_name: str | None = None,
259
+ ):
260
+ self.reserved_name = reserved_name
261
+ super().__init__(
262
+ message,
263
+ filename=filename,
264
+ error_code=ErrorCode.WINDOWS_RESERVED_NAME,
265
+ )
266
+
267
+
268
+ # ============================================================================
269
+ # File Content Exceptions
270
+ # ============================================================================
271
+
272
+
273
+ class FileSizeError(FileValidationError):
274
+ """
275
+ File exceeds configured size limits.
276
+
277
+ Args:
278
+ message: Human-readable error description.
279
+ filename: Optional filename that exceeded size limits.
280
+ size: Optional actual file size in bytes.
281
+ max_size: Optional maximum allowed size in bytes.
282
+
283
+ Attributes:
284
+ size: The actual file size in bytes.
285
+ max_size: The maximum allowed size in bytes.
286
+ """
287
+
288
+ def __init__(
289
+ self,
290
+ message: str,
291
+ filename: str | None = None,
292
+ size: int | None = None,
293
+ max_size: int | None = None,
294
+ ):
295
+ self.size = size
296
+ self.max_size = max_size
297
+ super().__init__(
298
+ message, filename=filename, error_code=ErrorCode.FILE_TOO_LARGE
299
+ )
300
+
301
+
302
+ class MimeTypeError(FileValidationError):
303
+ """
304
+ File MIME type not allowed or mismatches extension.
305
+
306
+ Args:
307
+ message: Human-readable error description.
308
+ filename: Optional filename with MIME type issue.
309
+ detected_mime: Optional detected MIME type string.
310
+ allowed_mimes: Optional list of allowed MIME types.
311
+ error_code: Optional error code (defaults to
312
+ MIME_TYPE_INVALID).
313
+
314
+ Attributes:
315
+ detected_mime: The detected MIME type string.
316
+ allowed_mimes: List of allowed MIME types.
317
+ """
318
+
319
+ def __init__(
320
+ self,
321
+ message: str,
322
+ filename: str | None = None,
323
+ detected_mime: str | None = None,
324
+ allowed_mimes: list[str] | None = None,
325
+ error_code: str | None = None,
326
+ ):
327
+ self.detected_mime = detected_mime
328
+ self.allowed_mimes = allowed_mimes or []
329
+ super().__init__(
330
+ message,
331
+ filename=filename,
332
+ error_code=error_code or ErrorCode.MIME_TYPE_INVALID,
333
+ )
334
+
335
+
336
+ class FileSignatureError(FileValidationError):
337
+ """
338
+ File header signature invalid or mismatched.
339
+
340
+ Args:
341
+ message: Human-readable error description.
342
+ filename: Optional filename with signature issue.
343
+ expected_type: Optional expected file type based on extension.
344
+
345
+ Attributes:
346
+ expected_type: The expected file type based on extension.
347
+ """
348
+
349
+ def __init__(
350
+ self,
351
+ message: str,
352
+ filename: str | None = None,
353
+ expected_type: str | None = None,
354
+ ):
355
+ self.expected_type = expected_type
356
+ super().__init__(
357
+ message,
358
+ filename=filename,
359
+ error_code=ErrorCode.FILE_SIGNATURE_MISMATCH,
360
+ )
361
+
362
+
363
+ # ============================================================================
364
+ # Compression and ZIP Exceptions
365
+ # ============================================================================
366
+
367
+
368
+ class CompressionSecurityError(FileValidationError):
369
+ """
370
+ Compressed file security check failed.
371
+
372
+ Args:
373
+ message: Human-readable error description.
374
+ filename: Optional filename of compressed file.
375
+ error_code: Optional error code (defaults to
376
+ COMPRESSION_GENERIC).
377
+
378
+ Attributes:
379
+ None beyond inherited FileValidationError attributes.
380
+ """
381
+
382
+ pass
383
+
384
+
385
+ class ZipBombError(CompressionSecurityError):
386
+ """
387
+ Zip archive exceeds compression ratio or uncompressed size limits.
388
+
389
+ Args:
390
+ message: Human-readable error description.
391
+ filename: Optional filename of zip bomb.
392
+ compression_ratio: Optional actual compression ratio detected.
393
+ uncompressed_size: Optional total uncompressed size in bytes.
394
+ max_ratio: Optional maximum allowed compression ratio.
395
+ max_size: Optional maximum allowed uncompressed size in bytes.
396
+
397
+ Attributes:
398
+ compression_ratio: Actual compression ratio detected.
399
+ uncompressed_size: Total uncompressed size in bytes.
400
+ max_ratio: Maximum allowed compression ratio.
401
+ max_size: Maximum allowed uncompressed size in bytes.
402
+ """
403
+
404
+ def __init__(
405
+ self,
406
+ message: str,
407
+ filename: str | None = None,
408
+ compression_ratio: float | None = None,
409
+ uncompressed_size: int | None = None,
410
+ max_ratio: float | None = None,
411
+ max_size: int | None = None,
412
+ ):
413
+ self.compression_ratio = compression_ratio
414
+ self.uncompressed_size = uncompressed_size
415
+ self.max_ratio = max_ratio
416
+ self.max_size = max_size
417
+ super().__init__(
418
+ message,
419
+ filename=filename,
420
+ error_code=ErrorCode.ZIP_BOMB_DETECTED,
421
+ )
422
+
423
+
424
+ class ZipContentError(CompressionSecurityError):
425
+ """
426
+ Zip archive contains dangerous content or structure.
427
+
428
+ Args:
429
+ message: Human-readable error description.
430
+ filename: Optional filename of problematic archive.
431
+ threats: Optional list of detected threat descriptions.
432
+ error_code: Optional error code (defaults to
433
+ ZIP_CONTENT_THREAT).
434
+
435
+ Attributes:
436
+ threats: List of detected threat descriptions.
437
+ """
438
+
439
+ def __init__(
440
+ self,
441
+ message: str,
442
+ filename: str | None = None,
443
+ threats: list[str] | None = None,
444
+ error_code: str | None = None,
445
+ ):
446
+ self.threats = threats or []
447
+ super().__init__(
448
+ message,
449
+ filename=filename,
450
+ error_code=error_code or ErrorCode.ZIP_CONTENT_THREAT,
451
+ )
452
+
453
+
454
+ class FileProcessingError(FileSecurityError):
455
+ """
456
+ Unexpected processing error during file validation.
457
+
458
+ Args:
459
+ message: Human-readable error description.
460
+ original_error: Optional original exception that was caught.
461
+
462
+ Attributes:
463
+ original_error: The original exception that was caught.
464
+ """
465
+
466
+ def __init__(self, message: str, original_error: Exception | None = None):
467
+ self.original_error = original_error
468
+ super().__init__(message, error_code=ErrorCode.PROCESSING_ERROR)