1use std::path::PathBuf;
4use thiserror::Error;
5
6pub type Result<T> = std::result::Result<T, Error>;
8
9#[derive(Debug, Error)]
11pub enum Error {
12 #[error("invalid ZIP archive: {0}")]
14 InvalidArchive(#[from] zip::result::ZipError),
15
16 #[error("JSON error: {0}")]
18 Json(#[from] serde_json::Error),
19
20 #[error("I/O error: {0}")]
22 Io(#[from] std::io::Error),
23
24 #[error("missing required file: {path}")]
26 MissingFile {
27 path: String,
29 },
30
31 #[error("invalid manifest: {reason}")]
33 InvalidManifest {
34 reason: String,
36 },
37
38 #[error("unsupported Codex version: {version}")]
40 UnsupportedVersion {
41 version: String,
43 },
44
45 #[error("hash mismatch for {path}: expected {expected}, got {actual}")]
47 HashMismatch {
48 path: String,
50 expected: String,
52 actual: String,
54 },
55
56 #[error("document ID mismatch: expected {expected}, got {actual}")]
58 DocumentIdMismatch {
59 expected: String,
61 actual: String,
63 },
64
65 #[error("invalid state transition from {from:?} to {to:?}")]
67 InvalidStateTransition {
68 from: crate::DocumentState,
70 to: crate::DocumentState,
72 },
73
74 #[error("state {state:?} requires {requirement}")]
76 StateRequirementNotMet {
77 state: crate::DocumentState,
79 requirement: String,
81 },
82
83 #[error("path traversal detected: {path}")]
85 PathTraversal {
86 path: String,
88 },
89
90 #[error("unsupported hash algorithm: {algorithm}")]
92 UnsupportedHashAlgorithm {
93 algorithm: String,
95 },
96
97 #[error("invalid hash format: {value}")]
99 InvalidHashFormat {
100 value: String,
102 },
103
104 #[error("file not found: {}", path.display())]
106 FileNotFound {
107 path: PathBuf,
109 },
110
111 #[error("invalid certificate: {reason}")]
113 InvalidCertificate {
114 reason: String,
116 },
117
118 #[error("network error: {message}")]
120 Network {
121 message: String,
123 },
124
125 #[error("not implemented: {feature}")]
127 NotImplemented {
128 feature: String,
130 },
131
132 #[error("cannot {action} in {state:?} state")]
134 ImmutableDocument {
135 action: String,
137 state: crate::DocumentState,
139 },
140
141 #[error("extension not available: {extension}")]
143 ExtensionNotAvailable {
144 extension: String,
146 },
147
148 #[error("content validation failed: {reason}")]
150 ValidationFailed {
151 reason: String,
153 },
154
155 #[error("signature error: {reason}")]
157 SignatureError {
158 reason: String,
160 },
161
162 #[error("encryption error: {reason}")]
164 EncryptionError {
165 reason: String,
167 },
168
169 #[error("file too large: {path} is {size} bytes (limit: {limit} bytes)")]
171 FileTooLarge {
172 path: String,
174 size: u64,
176 limit: u64,
178 },
179
180 #[error("invalid archive structure: {reason}")]
182 InvalidArchiveStructure {
183 reason: String,
185 },
186}
187
188pub(crate) fn invalid_manifest(reason: impl Into<String>) -> Error {
190 Error::InvalidManifest {
191 reason: reason.into(),
192 }
193}
194
195pub(crate) fn encryption_error(reason: impl Into<String>) -> Error {
197 Error::EncryptionError {
198 reason: reason.into(),
199 }
200}
201
202pub(crate) fn network_error(message: impl Into<String>) -> Error {
204 Error::Network {
205 message: message.into(),
206 }
207}
208
209pub(crate) fn invalid_certificate(reason: impl Into<String>) -> Error {
211 Error::InvalidCertificate {
212 reason: reason.into(),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use std::path::PathBuf;
220
221 #[test]
222 fn display_invalid_archive() {
223 let err = Error::InvalidArchive(zip::result::ZipError::FileNotFound);
224 assert!(err.to_string().contains("invalid ZIP archive"));
225 }
226
227 #[test]
228 fn display_json() {
229 let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
230 let err = Error::Json(json_err);
231 assert!(err.to_string().starts_with("JSON error:"));
232 }
233
234 #[test]
235 fn display_io() {
236 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
237 let err = Error::Io(io_err);
238 assert!(err.to_string().starts_with("I/O error:"));
239 }
240
241 #[test]
242 fn display_missing_file() {
243 let err = Error::MissingFile {
244 path: "manifest.json".to_string(),
245 };
246 assert_eq!(err.to_string(), "missing required file: manifest.json");
247 }
248
249 #[test]
250 fn display_invalid_manifest() {
251 let err = Error::InvalidManifest {
252 reason: "bad version".to_string(),
253 };
254 assert_eq!(err.to_string(), "invalid manifest: bad version");
255 }
256
257 #[test]
258 fn display_unsupported_version() {
259 let err = Error::UnsupportedVersion {
260 version: "99.0".to_string(),
261 };
262 assert_eq!(err.to_string(), "unsupported Codex version: 99.0");
263 }
264
265 #[test]
266 fn display_hash_mismatch() {
267 let err = Error::HashMismatch {
268 path: "content.json".to_string(),
269 expected: "abc".to_string(),
270 actual: "def".to_string(),
271 };
272 assert_eq!(
273 err.to_string(),
274 "hash mismatch for content.json: expected abc, got def"
275 );
276 }
277
278 #[test]
279 fn display_document_id_mismatch() {
280 let err = Error::DocumentIdMismatch {
281 expected: "id1".to_string(),
282 actual: "id2".to_string(),
283 };
284 assert_eq!(
285 err.to_string(),
286 "document ID mismatch: expected id1, got id2"
287 );
288 }
289
290 #[test]
291 fn display_invalid_state_transition() {
292 let err = Error::InvalidStateTransition {
293 from: crate::DocumentState::Draft,
294 to: crate::DocumentState::Frozen,
295 };
296 assert!(err.to_string().contains("invalid state transition"));
297 assert!(err.to_string().contains("Draft"));
298 assert!(err.to_string().contains("Frozen"));
299 }
300
301 #[test]
302 fn display_state_requirement_not_met() {
303 let err = Error::StateRequirementNotMet {
304 state: crate::DocumentState::Frozen,
305 requirement: "at least one signature".to_string(),
306 };
307 assert!(err.to_string().contains("Frozen"));
308 assert!(err.to_string().contains("at least one signature"));
309 }
310
311 #[test]
312 fn display_path_traversal() {
313 let err = Error::PathTraversal {
314 path: "../etc/passwd".to_string(),
315 };
316 assert_eq!(err.to_string(), "path traversal detected: ../etc/passwd");
317 }
318
319 #[test]
320 fn display_unsupported_hash_algorithm() {
321 let err = Error::UnsupportedHashAlgorithm {
322 algorithm: "md5".to_string(),
323 };
324 assert_eq!(err.to_string(), "unsupported hash algorithm: md5");
325 }
326
327 #[test]
328 fn display_invalid_hash_format() {
329 let err = Error::InvalidHashFormat {
330 value: "not-a-hash".to_string(),
331 };
332 assert_eq!(err.to_string(), "invalid hash format: not-a-hash");
333 }
334
335 #[test]
336 fn display_file_not_found() {
337 let err = Error::FileNotFound {
338 path: PathBuf::from("/tmp/missing.cdx"),
339 };
340 assert!(err.to_string().contains("file not found"));
341 assert!(err.to_string().contains("missing.cdx"));
342 }
343
344 #[test]
345 fn display_invalid_certificate() {
346 let err = Error::InvalidCertificate {
347 reason: "expired".to_string(),
348 };
349 assert_eq!(err.to_string(), "invalid certificate: expired");
350 }
351
352 #[test]
353 fn display_network() {
354 let err = Error::Network {
355 message: "timeout".to_string(),
356 };
357 assert_eq!(err.to_string(), "network error: timeout");
358 }
359
360 #[test]
361 fn display_not_implemented() {
362 let err = Error::NotImplemented {
363 feature: "blockchain anchoring".to_string(),
364 };
365 assert_eq!(err.to_string(), "not implemented: blockchain anchoring");
366 }
367
368 #[test]
369 fn display_immutable_document() {
370 let err = Error::ImmutableDocument {
371 action: "modify content".to_string(),
372 state: crate::DocumentState::Frozen,
373 };
374 assert!(err.to_string().contains("cannot modify content"));
375 assert!(err.to_string().contains("Frozen"));
376 }
377
378 #[test]
379 fn display_extension_not_available() {
380 let err = Error::ExtensionNotAvailable {
381 extension: "forms".to_string(),
382 };
383 assert_eq!(err.to_string(), "extension not available: forms");
384 }
385
386 #[test]
387 fn display_validation_failed() {
388 let err = Error::ValidationFailed {
389 reason: "empty content".to_string(),
390 };
391 assert_eq!(err.to_string(), "content validation failed: empty content");
392 }
393
394 #[test]
395 fn display_signature_error() {
396 let err = Error::SignatureError {
397 reason: "invalid key".to_string(),
398 };
399 assert_eq!(err.to_string(), "signature error: invalid key");
400 }
401
402 #[test]
403 fn display_encryption_error() {
404 let err = Error::EncryptionError {
405 reason: "wrong password".to_string(),
406 };
407 assert_eq!(err.to_string(), "encryption error: wrong password");
408 }
409
410 #[test]
411 fn display_file_too_large() {
412 let err = Error::FileTooLarge {
413 path: "assets/huge.bin".to_string(),
414 size: 512 * 1024 * 1024,
415 limit: 256 * 1024 * 1024,
416 };
417 let msg = err.to_string();
418 assert!(msg.contains("file too large"));
419 assert!(msg.contains("assets/huge.bin"));
420 }
421
422 #[test]
423 fn display_invalid_archive_structure() {
424 let err = Error::InvalidArchiveStructure {
425 reason: "manifest not first".to_string(),
426 };
427 assert_eq!(
428 err.to_string(),
429 "invalid archive structure: manifest not first"
430 );
431 }
432}