1use oxigdal_core::error::OxiGdalError;
7use thiserror::Error;
8
9pub type Result<T> = core::result::Result<T, ShapefileError>;
11
12#[derive(Debug, Error)]
14pub enum ShapefileError {
15 #[error("Invalid Shapefile header: {message}")]
17 InvalidHeader {
18 message: String,
20 },
21
22 #[error("Invalid file code: expected 9994, got {actual}")]
24 InvalidFileCode {
25 actual: i32,
27 },
28
29 #[error("Invalid version: {version}")]
31 InvalidVersion {
32 version: i32,
34 },
35
36 #[error("Unsupported shape type: {shape_type}")]
38 UnsupportedShapeType {
39 shape_type: i32,
41 },
42
43 #[error("Invalid shape type: {shape_type}")]
45 InvalidShapeType {
46 shape_type: i32,
48 },
49
50 #[error("Invalid geometry: {message}")]
52 InvalidGeometry {
53 message: String,
55 record: Option<usize>,
57 },
58
59 #[error("Invalid coordinates: {message}")]
61 InvalidCoordinates {
62 message: String,
64 position: Option<usize>,
66 },
67
68 #[error("Invalid bounding box: {message}")]
70 InvalidBbox {
71 message: String,
73 },
74
75 #[error("DBF error: {message}")]
77 DbfError {
78 message: String,
80 field: Option<String>,
82 record: Option<usize>,
84 },
85
86 #[error("Invalid DBF header: {message}")]
88 InvalidDbfHeader {
89 message: String,
91 },
92
93 #[error("Invalid field descriptor: {message}")]
95 InvalidFieldDescriptor {
96 message: String,
98 field: Option<String>,
100 },
101
102 #[error("Invalid field value: {message}")]
104 InvalidFieldValue {
105 message: String,
107 field: String,
109 record: usize,
111 },
112
113 #[error("Encoding error: {message}")]
115 EncodingError {
116 message: String,
118 code_page: Option<u8>,
120 },
121
122 #[error("SHX index error: {message}")]
124 ShxError {
125 message: String,
127 record: Option<usize>,
129 },
130
131 #[error("Record count mismatch: .shp has {shp_count} records, .dbf has {dbf_count} records")]
133 RecordMismatch {
134 shp_count: usize,
136 dbf_count: usize,
138 },
139
140 #[error("Missing required file: {file_type}")]
142 MissingFile {
143 file_type: String,
145 },
146
147 #[error("I/O error: {0}")]
149 Io(#[from] std::io::Error),
150
151 #[error("Unexpected end of file: {message}")]
153 UnexpectedEof {
154 message: String,
156 },
157
158 #[error("Validation error: {message}")]
160 Validation {
161 message: String,
163 path: Option<String>,
165 },
166
167 #[error("Topology error: {message}")]
169 Topology {
170 message: String,
172 },
173
174 #[error("Out of memory: {message}")]
176 OutOfMemory {
177 message: String,
179 },
180
181 #[error("Limit exceeded: {message}")]
183 LimitExceeded {
184 message: String,
186 limit: usize,
188 actual: usize,
190 },
191
192 #[error("OxiGDAL error: {0}")]
194 OxiGdal(#[from] OxiGdalError),
195}
196
197impl ShapefileError {
198 pub fn invalid_header<S: Into<String>>(message: S) -> Self {
200 Self::InvalidHeader {
201 message: message.into(),
202 }
203 }
204
205 pub fn invalid_geometry<S: Into<String>>(message: S) -> Self {
207 Self::InvalidGeometry {
208 message: message.into(),
209 record: None,
210 }
211 }
212
213 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 pub fn invalid_coordinates<S: Into<String>>(message: S) -> Self {
223 Self::InvalidCoordinates {
224 message: message.into(),
225 position: None,
226 }
227 }
228
229 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 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 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 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 pub fn validation<S: Into<String>>(message: S) -> Self {
269 Self::Validation {
270 message: message.into(),
271 path: None,
272 }
273 }
274
275 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 pub fn topology<S: Into<String>>(message: S) -> Self {
285 Self::Topology {
286 message: message.into(),
287 }
288 }
289
290 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 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}