Skip to main content

oxigdal_shapefile/
error.rs

1//! Error types for Shapefile operations
2//!
3//! This module provides comprehensive error handling for all Shapefile operations,
4//! including parsing .shp, .dbf, and .shx files, validation, reading, and writing.
5
6use oxigdal_core::error::OxiGdalError;
7use thiserror::Error;
8
9/// Result type for Shapefile operations
10pub type Result<T> = core::result::Result<T, ShapefileError>;
11
12/// Comprehensive error type for Shapefile operations
13#[derive(Debug, Error)]
14pub enum ShapefileError {
15    /// Invalid Shapefile header
16    #[error("Invalid Shapefile header: {message}")]
17    InvalidHeader {
18        /// Error message
19        message: String,
20    },
21
22    /// Invalid file code
23    #[error("Invalid file code: expected 9994, got {actual}")]
24    InvalidFileCode {
25        /// Actual file code encountered
26        actual: i32,
27    },
28
29    /// Invalid version
30    #[error("Invalid version: {version}")]
31    InvalidVersion {
32        /// The invalid version number
33        version: i32,
34    },
35
36    /// Unsupported shape type
37    #[error("Unsupported shape type: {shape_type}")]
38    UnsupportedShapeType {
39        /// The unsupported shape type code
40        shape_type: i32,
41    },
42
43    /// Invalid shape type
44    #[error("Invalid shape type: {shape_type}")]
45    InvalidShapeType {
46        /// The invalid shape type code
47        shape_type: i32,
48    },
49
50    /// Invalid geometry
51    #[error("Invalid geometry: {message}")]
52    InvalidGeometry {
53        /// Error message
54        message: String,
55        /// Record number (if available)
56        record: Option<usize>,
57    },
58
59    /// Invalid coordinates
60    #[error("Invalid coordinates: {message}")]
61    InvalidCoordinates {
62        /// Error message
63        message: String,
64        /// Position in coordinate array (if known)
65        position: Option<usize>,
66    },
67
68    /// Invalid bounding box
69    #[error("Invalid bounding box: {message}")]
70    InvalidBbox {
71        /// Error message
72        message: String,
73    },
74
75    /// DBF parsing error
76    #[error("DBF error: {message}")]
77    DbfError {
78        /// Error message
79        message: String,
80        /// Field name (if applicable)
81        field: Option<String>,
82        /// Record number (if applicable)
83        record: Option<usize>,
84    },
85
86    /// Invalid DBF header
87    #[error("Invalid DBF header: {message}")]
88    InvalidDbfHeader {
89        /// Error message
90        message: String,
91    },
92
93    /// Invalid field descriptor
94    #[error("Invalid field descriptor: {message}")]
95    InvalidFieldDescriptor {
96        /// Error message
97        message: String,
98        /// Field name (if known)
99        field: Option<String>,
100    },
101
102    /// Invalid field value
103    #[error("Invalid field value: {message}")]
104    InvalidFieldValue {
105        /// Error message
106        message: String,
107        /// Field name
108        field: String,
109        /// Record number
110        record: usize,
111    },
112
113    /// Encoding error
114    #[error("Encoding error: {message}")]
115    EncodingError {
116        /// Error message
117        message: String,
118        /// Code page (if known)
119        code_page: Option<u8>,
120    },
121
122    /// SHX index error
123    #[error("SHX index error: {message}")]
124    ShxError {
125        /// Error message
126        message: String,
127        /// Record number (if applicable)
128        record: Option<usize>,
129    },
130
131    /// Record mismatch between .shp and .dbf
132    #[error("Record count mismatch: .shp has {shp_count} records, .dbf has {dbf_count} records")]
133    RecordMismatch {
134        /// Number of records in .shp file
135        shp_count: usize,
136        /// Number of records in .dbf file
137        dbf_count: usize,
138    },
139
140    /// Missing required file
141    #[error("Missing required file: {file_type}")]
142    MissingFile {
143        /// File type (e.g., ".shp", ".dbf", ".shx")
144        file_type: String,
145    },
146
147    /// I/O error
148    #[error("I/O error: {0}")]
149    Io(#[from] std::io::Error),
150
151    /// EOF (End of File) error
152    #[error("Unexpected end of file: {message}")]
153    UnexpectedEof {
154        /// Error message
155        message: String,
156    },
157
158    /// Validation error
159    #[error("Validation error: {message}")]
160    Validation {
161        /// Error message
162        message: String,
163        /// Path to the invalid element
164        path: Option<String>,
165    },
166
167    /// Topology error
168    #[error("Topology error: {message}")]
169    Topology {
170        /// Error message
171        message: String,
172    },
173
174    /// Out of memory error
175    #[error("Out of memory: {message}")]
176    OutOfMemory {
177        /// Error message
178        message: String,
179    },
180
181    /// Limit exceeded
182    #[error("Limit exceeded: {message}")]
183    LimitExceeded {
184        /// Error message
185        message: String,
186        /// The limit that was exceeded
187        limit: usize,
188        /// The actual value
189        actual: usize,
190    },
191
192    /// Generic OxiGDAL error
193    #[error("OxiGDAL error: {0}")]
194    OxiGdal(#[from] OxiGdalError),
195}
196
197impl ShapefileError {
198    /// Creates a new invalid header error
199    pub fn invalid_header<S: Into<String>>(message: S) -> Self {
200        Self::InvalidHeader {
201            message: message.into(),
202        }
203    }
204
205    /// Creates a new invalid geometry error
206    pub fn invalid_geometry<S: Into<String>>(message: S) -> Self {
207        Self::InvalidGeometry {
208            message: message.into(),
209            record: None,
210        }
211    }
212
213    /// Creates a new invalid geometry error with record number
214    pub fn invalid_geometry_at<S: Into<String>>(message: S, record: usize) -> Self {
215        Self::InvalidGeometry {
216            message: message.into(),
217            record: Some(record),
218        }
219    }
220
221    /// Creates a new invalid coordinates error
222    pub fn invalid_coordinates<S: Into<String>>(message: S) -> Self {
223        Self::InvalidCoordinates {
224            message: message.into(),
225            position: None,
226        }
227    }
228
229    /// Creates a new invalid coordinates error with position
230    pub fn invalid_coordinates_at<S: Into<String>>(message: S, position: usize) -> Self {
231        Self::InvalidCoordinates {
232            message: message.into(),
233            position: Some(position),
234        }
235    }
236
237    /// Creates a new DBF error
238    pub fn dbf_error<S: Into<String>>(message: S) -> Self {
239        Self::DbfError {
240            message: message.into(),
241            field: None,
242            record: None,
243        }
244    }
245
246    /// Creates a new DBF error with field and record
247    pub fn dbf_error_at<S: Into<String>, F: Into<String>>(
248        message: S,
249        field: F,
250        record: usize,
251    ) -> Self {
252        Self::DbfError {
253            message: message.into(),
254            field: Some(field.into()),
255            record: Some(record),
256        }
257    }
258
259    /// Creates a new encoding error
260    pub fn encoding_error<S: Into<String>>(message: S) -> Self {
261        Self::EncodingError {
262            message: message.into(),
263            code_page: None,
264        }
265    }
266
267    /// Creates a new validation error
268    pub fn validation<S: Into<String>>(message: S) -> Self {
269        Self::Validation {
270            message: message.into(),
271            path: None,
272        }
273    }
274
275    /// Creates a new validation error with path
276    pub fn validation_at<S: Into<String>, P: Into<String>>(message: S, path: P) -> Self {
277        Self::Validation {
278            message: message.into(),
279            path: Some(path.into()),
280        }
281    }
282
283    /// Creates a new topology error
284    pub fn topology<S: Into<String>>(message: S) -> Self {
285        Self::Topology {
286            message: message.into(),
287        }
288    }
289
290    /// Creates a new limit exceeded error
291    pub fn limit_exceeded<S: Into<String>>(message: S, limit: usize, actual: usize) -> Self {
292        Self::LimitExceeded {
293            message: message.into(),
294            limit,
295            actual,
296        }
297    }
298
299    /// Creates a new unexpected EOF error
300    pub fn unexpected_eof<S: Into<String>>(message: S) -> Self {
301        Self::UnexpectedEof {
302            message: message.into(),
303        }
304    }
305}
306
307#[cfg(test)]
308#[allow(clippy::panic)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_error_display() {
314        let err = ShapefileError::invalid_header("missing file code");
315        assert!(err.to_string().contains("missing file code"));
316
317        let err = ShapefileError::invalid_geometry_at("invalid polygon", 5);
318        assert!(err.to_string().contains("invalid polygon"));
319
320        let err = ShapefileError::InvalidFileCode { actual: 1234 };
321        assert!(err.to_string().contains("9994"));
322        assert!(err.to_string().contains("1234"));
323    }
324
325    #[test]
326    fn test_record_mismatch() {
327        let err = ShapefileError::RecordMismatch {
328            shp_count: 100,
329            dbf_count: 95,
330        };
331        assert!(err.to_string().contains("100"));
332        assert!(err.to_string().contains("95"));
333    }
334
335    #[test]
336    fn test_dbf_error_construction() {
337        let err = ShapefileError::dbf_error_at("invalid value", "name", 42);
338        if let ShapefileError::DbfError {
339            field,
340            record,
341            message,
342        } = err
343        {
344            assert_eq!(field, Some("name".to_string()));
345            assert_eq!(record, Some(42));
346            assert_eq!(message, "invalid value");
347        } else {
348            panic!("Expected DbfError");
349        }
350    }
351}