zarja_core/
error.rs

1//! Error types for the zarja-core library.
2//!
3//! This module provides comprehensive error handling using the `thiserror` crate,
4//! with detailed error variants for different failure modes.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Result type alias for zarja operations
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// Comprehensive error type for all zarja operations
13#[derive(Error, Debug)]
14#[non_exhaustive]
15pub enum Error {
16    /// Failed to read input file
17    #[error("failed to read file '{path}': {source}")]
18    FileRead {
19        /// Path to the file that failed to read
20        path: PathBuf,
21        /// Underlying I/O error
22        #[source]
23        source: std::io::Error,
24    },
25
26    /// Failed to write output file
27    #[error("failed to write file '{path}': {source}")]
28    FileWrite {
29        /// Path to the file that failed to write
30        path: PathBuf,
31        /// Underlying I/O error
32        #[source]
33        source: std::io::Error,
34    },
35
36    /// Failed to create output directory
37    #[error("failed to create directory '{path}': {source}")]
38    DirectoryCreate {
39        /// Path to the directory that failed to create
40        path: PathBuf,
41        /// Underlying I/O error
42        #[source]
43        source: std::io::Error,
44    },
45
46    /// Path traversal attempt detected (security error)
47    #[error("path traversal detected: '{path}' would escape output directory")]
48    PathTraversal {
49        /// The suspicious path
50        path: PathBuf,
51    },
52
53    /// Invalid protobuf wire format
54    #[error("invalid protobuf wire format at offset {offset}: {details}")]
55    InvalidWireFormat {
56        /// Byte offset where the error occurred
57        offset: usize,
58        /// Detailed description of the issue
59        details: String,
60    },
61
62    /// Failed to decode varint
63    #[error("failed to decode varint at offset {offset}: buffer too small or invalid encoding")]
64    VarintDecode {
65        /// Byte offset where the error occurred
66        offset: usize,
67    },
68
69    /// Failed to parse FileDescriptorProto
70    #[error("failed to parse FileDescriptorProto: {0}")]
71    DescriptorParse(#[from] prost::DecodeError),
72
73    /// Failed to build file descriptor with prost-reflect
74    #[error("failed to build file descriptor: {0}")]
75    DescriptorBuild(String),
76
77    /// No descriptors found in input
78    #[error("no protobuf descriptors found in input")]
79    NoDescriptorsFound,
80
81    /// Invalid field number in descriptor
82    #[error("invalid field number {number}: must be between 1 and {max}")]
83    InvalidFieldNumber {
84        /// The invalid field number
85        number: u32,
86        /// Maximum valid field number
87        max: u32,
88    },
89
90    /// Unsupported proto syntax version
91    #[error("unsupported proto syntax: '{syntax}'")]
92    UnsupportedSyntax {
93        /// The unsupported syntax string
94        syntax: String,
95    },
96
97    /// Generic internal error
98    #[error("internal error: {0}")]
99    Internal(String),
100}
101
102impl Error {
103    /// Creates a new file read error
104    pub fn file_read(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
105        Self::FileRead {
106            path: path.into(),
107            source,
108        }
109    }
110
111    /// Creates a new file write error
112    pub fn file_write(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
113        Self::FileWrite {
114            path: path.into(),
115            source,
116        }
117    }
118
119    /// Creates a new directory creation error
120    pub fn directory_create(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
121        Self::DirectoryCreate {
122            path: path.into(),
123            source,
124        }
125    }
126
127    /// Creates a new path traversal error
128    pub fn path_traversal(path: impl Into<PathBuf>) -> Self {
129        Self::PathTraversal { path: path.into() }
130    }
131
132    /// Creates a new wire format error
133    pub fn invalid_wire_format(offset: usize, details: impl Into<String>) -> Self {
134        Self::InvalidWireFormat {
135            offset,
136            details: details.into(),
137        }
138    }
139
140    /// Creates a new varint decode error
141    pub fn varint_decode(offset: usize) -> Self {
142        Self::VarintDecode { offset }
143    }
144
145    /// Creates a new descriptor build error
146    pub fn descriptor_build(msg: impl Into<String>) -> Self {
147        Self::DescriptorBuild(msg.into())
148    }
149
150    /// Creates a new internal error
151    pub fn internal(msg: impl Into<String>) -> Self {
152        Self::Internal(msg.into())
153    }
154
155    /// Returns true if this is a recoverable error that should be skipped
156    pub fn is_recoverable(&self) -> bool {
157        matches!(
158            self,
159            Self::DescriptorParse(_) | Self::DescriptorBuild(_) | Self::InvalidWireFormat { .. }
160        )
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_error_display() {
170        let err = Error::path_traversal("/etc/passwd");
171        assert!(err.to_string().contains("path traversal"));
172        assert!(err.to_string().contains("/etc/passwd"));
173    }
174
175    #[test]
176    fn test_is_recoverable() {
177        assert!(Error::descriptor_build("test").is_recoverable());
178        assert!(!Error::path_traversal("/test").is_recoverable());
179    }
180}