alopex_chirps_file_transfer/
error.rs

1use crate::{ChunkIndex, TransferSessionId};
2use thiserror::Error;
3
4/// Errors returned by file transfer operations.
5#[derive(Debug, Error)]
6pub enum FileTransferError {
7    // Session errors (1xxx)
8    #[error("session not found: {0}")]
9    SessionNotFound(TransferSessionId),
10    #[error("session already exists: {0}")]
11    SessionAlreadyExists(TransferSessionId),
12    #[error("invalid session state")]
13    InvalidState { expected: String, actual: String },
14
15    // File errors (2xxx)
16    #[error("file not found: {0}")]
17    FileNotFound(String),
18    #[error("file already exists: {0}")]
19    FileAlreadyExists(String),
20    #[error("permission denied: {0}")]
21    PermissionDenied(String),
22    #[error("disk full")]
23    DiskFull,
24    #[error("path traversal attack detected: {0}")]
25    PathTraversal(String),
26
27    // Transfer errors (3xxx)
28    #[error("chunk checksum mismatch at index {index}")]
29    ChunkChecksumMismatch { index: ChunkIndex },
30    #[error("file hash mismatch")]
31    FileHashMismatch,
32    #[error("transfer timeout")]
33    Timeout,
34    #[error("transfer cancelled")]
35    Cancelled,
36    #[error("max retries exceeded for chunk {index}")]
37    MaxRetriesExceeded { index: ChunkIndex },
38
39    // Sync errors (4xxx)
40    #[error("sync conflict: {path}")]
41    SyncConflict { path: String },
42
43    // Internal errors (9xxx)
44    #[error("io error: {0}")]
45    Io(#[from] std::io::Error),
46    #[error("transport error: {0}")]
47    Transport(String),
48    #[error("peer rejected: {0}")]
49    Rejected(String),
50    #[error("serialization error: {0}")]
51    Serialization(String),
52    #[error("compression error: {0}")]
53    Compression(String),
54    #[error("internal error: {0}")]
55    Internal(String),
56}
57
58impl FileTransferError {
59    /// Returns the numeric error code used by the wire protocol.
60    ///
61    /// # Panics
62    /// This method does not panic.
63    pub fn code(&self) -> u32 {
64        match self {
65            FileTransferError::SessionNotFound(_) => 1001,
66            FileTransferError::SessionAlreadyExists(_) => 1002,
67            FileTransferError::InvalidState { .. } => 1003,
68            FileTransferError::FileNotFound(_) => 2001,
69            FileTransferError::FileAlreadyExists(_) => 2002,
70            FileTransferError::PermissionDenied(_) => 2003,
71            FileTransferError::DiskFull => 2004,
72            FileTransferError::PathTraversal(_) => 2005,
73            FileTransferError::ChunkChecksumMismatch { .. } => 3001,
74            FileTransferError::FileHashMismatch => 3002,
75            FileTransferError::Timeout => 3003,
76            FileTransferError::Cancelled => 3004,
77            FileTransferError::MaxRetriesExceeded { .. } => 3005,
78            FileTransferError::SyncConflict { .. } => 4001,
79            FileTransferError::Io(_)
80            | FileTransferError::Transport(_)
81            | FileTransferError::Rejected(_)
82            | FileTransferError::Serialization(_)
83            | FileTransferError::Compression(_)
84            | FileTransferError::Internal(_) => 9001,
85        }
86    }
87
88    /// Returns true if the error is considered recoverable by retry logic.
89    ///
90    /// # Panics
91    /// This method does not panic.
92    pub fn is_recoverable(&self) -> bool {
93        matches!(
94            self,
95            FileTransferError::ChunkChecksumMismatch { .. }
96                | FileTransferError::Timeout
97                | FileTransferError::Transport(_)
98        )
99    }
100}