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 signature_error(reason: impl Into<String>) -> Error {
197 Error::SignatureError {
198 reason: reason.into(),
199 }
200}
201
202pub(crate) fn encryption_error(reason: impl Into<String>) -> Error {
204 Error::EncryptionError {
205 reason: reason.into(),
206 }
207}
208
209pub(crate) fn network_error(message: impl Into<String>) -> Error {
211 Error::Network {
212 message: message.into(),
213 }
214}
215
216pub(crate) fn invalid_certificate(reason: impl Into<String>) -> Error {
218 Error::InvalidCertificate {
219 reason: reason.into(),
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use std::path::PathBuf;
227
228 #[test]
229 fn display_invalid_archive() {
230 let err = Error::InvalidArchive(zip::result::ZipError::FileNotFound);
231 assert!(err.to_string().contains("invalid ZIP archive"));
232 }
233
234 #[test]
235 fn display_json() {
236 let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
237 let err = Error::Json(json_err);
238 assert!(err.to_string().starts_with("JSON error:"));
239 }
240
241 #[test]
242 fn display_io() {
243 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
244 let err = Error::Io(io_err);
245 assert!(err.to_string().starts_with("I/O error:"));
246 }
247
248 #[test]
249 fn display_missing_file() {
250 let err = Error::MissingFile {
251 path: "manifest.json".to_string(),
252 };
253 assert_eq!(err.to_string(), "missing required file: manifest.json");
254 }
255
256 #[test]
257 fn display_invalid_manifest() {
258 let err = Error::InvalidManifest {
259 reason: "bad version".to_string(),
260 };
261 assert_eq!(err.to_string(), "invalid manifest: bad version");
262 }
263
264 #[test]
265 fn display_unsupported_version() {
266 let err = Error::UnsupportedVersion {
267 version: "99.0".to_string(),
268 };
269 assert_eq!(err.to_string(), "unsupported Codex version: 99.0");
270 }
271
272 #[test]
273 fn display_hash_mismatch() {
274 let err = Error::HashMismatch {
275 path: "content.json".to_string(),
276 expected: "abc".to_string(),
277 actual: "def".to_string(),
278 };
279 assert_eq!(
280 err.to_string(),
281 "hash mismatch for content.json: expected abc, got def"
282 );
283 }
284
285 #[test]
286 fn display_document_id_mismatch() {
287 let err = Error::DocumentIdMismatch {
288 expected: "id1".to_string(),
289 actual: "id2".to_string(),
290 };
291 assert_eq!(
292 err.to_string(),
293 "document ID mismatch: expected id1, got id2"
294 );
295 }
296
297 #[test]
298 fn display_invalid_state_transition() {
299 let err = Error::InvalidStateTransition {
300 from: crate::DocumentState::Draft,
301 to: crate::DocumentState::Frozen,
302 };
303 assert!(err.to_string().contains("invalid state transition"));
304 assert!(err.to_string().contains("Draft"));
305 assert!(err.to_string().contains("Frozen"));
306 }
307
308 #[test]
309 fn display_state_requirement_not_met() {
310 let err = Error::StateRequirementNotMet {
311 state: crate::DocumentState::Frozen,
312 requirement: "at least one signature".to_string(),
313 };
314 assert!(err.to_string().contains("Frozen"));
315 assert!(err.to_string().contains("at least one signature"));
316 }
317
318 #[test]
319 fn display_path_traversal() {
320 let err = Error::PathTraversal {
321 path: "../etc/passwd".to_string(),
322 };
323 assert_eq!(err.to_string(), "path traversal detected: ../etc/passwd");
324 }
325
326 #[test]
327 fn display_unsupported_hash_algorithm() {
328 let err = Error::UnsupportedHashAlgorithm {
329 algorithm: "md5".to_string(),
330 };
331 assert_eq!(err.to_string(), "unsupported hash algorithm: md5");
332 }
333
334 #[test]
335 fn display_invalid_hash_format() {
336 let err = Error::InvalidHashFormat {
337 value: "not-a-hash".to_string(),
338 };
339 assert_eq!(err.to_string(), "invalid hash format: not-a-hash");
340 }
341
342 #[test]
343 fn display_file_not_found() {
344 let err = Error::FileNotFound {
345 path: PathBuf::from("/tmp/missing.cdx"),
346 };
347 assert!(err.to_string().contains("file not found"));
348 assert!(err.to_string().contains("missing.cdx"));
349 }
350
351 #[test]
352 fn display_invalid_certificate() {
353 let err = Error::InvalidCertificate {
354 reason: "expired".to_string(),
355 };
356 assert_eq!(err.to_string(), "invalid certificate: expired");
357 }
358
359 #[test]
360 fn display_network() {
361 let err = Error::Network {
362 message: "timeout".to_string(),
363 };
364 assert_eq!(err.to_string(), "network error: timeout");
365 }
366
367 #[test]
368 fn display_not_implemented() {
369 let err = Error::NotImplemented {
370 feature: "blockchain anchoring".to_string(),
371 };
372 assert_eq!(err.to_string(), "not implemented: blockchain anchoring");
373 }
374
375 #[test]
376 fn display_immutable_document() {
377 let err = Error::ImmutableDocument {
378 action: "modify content".to_string(),
379 state: crate::DocumentState::Frozen,
380 };
381 assert!(err.to_string().contains("cannot modify content"));
382 assert!(err.to_string().contains("Frozen"));
383 }
384
385 #[test]
386 fn display_extension_not_available() {
387 let err = Error::ExtensionNotAvailable {
388 extension: "forms".to_string(),
389 };
390 assert_eq!(err.to_string(), "extension not available: forms");
391 }
392
393 #[test]
394 fn display_validation_failed() {
395 let err = Error::ValidationFailed {
396 reason: "empty content".to_string(),
397 };
398 assert_eq!(err.to_string(), "content validation failed: empty content");
399 }
400
401 #[test]
402 fn display_signature_error() {
403 let err = Error::SignatureError {
404 reason: "invalid key".to_string(),
405 };
406 assert_eq!(err.to_string(), "signature error: invalid key");
407 }
408
409 #[test]
410 fn display_encryption_error() {
411 let err = Error::EncryptionError {
412 reason: "wrong password".to_string(),
413 };
414 assert_eq!(err.to_string(), "encryption error: wrong password");
415 }
416
417 #[test]
418 fn display_file_too_large() {
419 let err = Error::FileTooLarge {
420 path: "assets/huge.bin".to_string(),
421 size: 512 * 1024 * 1024,
422 limit: 256 * 1024 * 1024,
423 };
424 let msg = err.to_string();
425 assert!(msg.contains("file too large"));
426 assert!(msg.contains("assets/huge.bin"));
427 }
428
429 #[test]
430 fn display_invalid_archive_structure() {
431 let err = Error::InvalidArchiveStructure {
432 reason: "manifest not first".to_string(),
433 };
434 assert_eq!(
435 err.to_string(),
436 "invalid archive structure: manifest not first"
437 );
438 }
439}