Skip to main content

geonative_shapefile/
header.rs

1//! The 100-byte file header shared by `.shp` and `.shx`.
2//!
3//! Layout per Esri J-7855 (July 1998):
4//!
5//! | Bytes | Field | Endian |
6//! | --- | --- | --- |
7//! | 0..4 | File code 9994 | Big |
8//! | 24..28 | File length in 16-bit words | Big |
9//! | 28..32 | Version 1000 | Little |
10//! | 32..36 | Shape type | Little |
11//! | 36..68 | XY bbox (xmin,ymin,xmax,ymax) | Little |
12//! | 68..100 | Z + M bbox | Little |
13
14use crate::bytes::Cursor;
15use crate::error::{Result, ShpError};
16
17pub const SHP_FILE_CODE: i32 = 9994;
18pub const SHP_VERSION: i32 = 1000;
19pub const SHP_HEADER_BYTES: usize = 100;
20
21/// Esri Shapefile shape type codes (2D + Z/M variants). v0.1 of this crate
22/// only decodes the four 2D variants (`Point`, `Polyline`, `Polygon`,
23/// `Multipoint`); Z/M / MultiPatch return [`ShpError::Unsupported`] at the
24/// decoder.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum ShapeType {
27    Null = 0,
28    Point = 1,
29    Polyline = 3,
30    Polygon = 5,
31    Multipoint = 8,
32    PointZ = 11,
33    PolylineZ = 13,
34    PolygonZ = 15,
35    MultipointZ = 18,
36    PointM = 21,
37    PolylineM = 23,
38    PolygonM = 25,
39    MultipointM = 28,
40    Multipatch = 31,
41}
42
43impl ShapeType {
44    pub fn from_i32(v: i32) -> Result<Self> {
45        Ok(match v {
46            0 => Self::Null,
47            1 => Self::Point,
48            3 => Self::Polyline,
49            5 => Self::Polygon,
50            8 => Self::Multipoint,
51            11 => Self::PointZ,
52            13 => Self::PolylineZ,
53            15 => Self::PolygonZ,
54            18 => Self::MultipointZ,
55            21 => Self::PointM,
56            23 => Self::PolylineM,
57            25 => Self::PolygonM,
58            28 => Self::MultipointM,
59            31 => Self::Multipatch,
60            other => return Err(ShpError::malformed(format!("unknown shape type {other}"))),
61        })
62    }
63}
64
65/// Parsed `.shp` / `.shx` file header (the format is byte-identical between
66/// the two files; only the records that follow differ).
67#[derive(Debug, Clone)]
68pub struct ShpHeader {
69    pub file_length_words: i32,
70    pub shape_type: ShapeType,
71    /// `[xmin, ymin, xmax, ymax]`.
72    pub bbox_xy: [f64; 4],
73    /// `[zmin, zmax]`. Zero for 2D files.
74    pub bbox_z: [f64; 2],
75    /// `[mmin, mmax]`. Zero / sentinel for non-M files.
76    pub bbox_m: [f64; 2],
77}
78
79pub fn parse(bytes: &[u8]) -> Result<ShpHeader> {
80    if bytes.len() < SHP_HEADER_BYTES {
81        return Err(ShpError::malformed(format!(
82            "file shorter than 100-byte header (got {})",
83            bytes.len()
84        )));
85    }
86    let mut c = Cursor::new(&bytes[..SHP_HEADER_BYTES]);
87    let code = c.read_i32_be()?;
88    if code != SHP_FILE_CODE {
89        return Err(ShpError::malformed(format!(
90            "bad file code {code:#x} (expected {SHP_FILE_CODE:#x})"
91        )));
92    }
93    // Skip 5 unused i32s (bytes 4..24).
94    c.seek(24)?;
95    let file_length_words = c.read_i32_be()?;
96    let version = c.read_i32_le()?;
97    if version != SHP_VERSION {
98        return Err(ShpError::malformed(format!(
99            "bad version {version} (expected {SHP_VERSION})"
100        )));
101    }
102    let shape_type = ShapeType::from_i32(c.read_i32_le()?)?;
103    let bbox_xy = [
104        c.read_f64_le()?,
105        c.read_f64_le()?,
106        c.read_f64_le()?,
107        c.read_f64_le()?,
108    ];
109    let bbox_z = [c.read_f64_le()?, c.read_f64_le()?];
110    let bbox_m = [c.read_f64_le()?, c.read_f64_le()?];
111    Ok(ShpHeader {
112        file_length_words,
113        shape_type,
114        bbox_xy,
115        bbox_z,
116        bbox_m,
117    })
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn synth_polygon_header() {
126        let mut h = vec![0u8; 100];
127        h[0..4].copy_from_slice(&SHP_FILE_CODE.to_be_bytes());
128        h[24..28].copy_from_slice(&500i32.to_be_bytes()); // file_length_words
129        h[28..32].copy_from_slice(&SHP_VERSION.to_le_bytes());
130        h[32..36].copy_from_slice(&5i32.to_le_bytes()); // Polygon
131        h[36..44].copy_from_slice(&0.0f64.to_le_bytes());
132        h[44..52].copy_from_slice(&0.0f64.to_le_bytes());
133        h[52..60].copy_from_slice(&10.0f64.to_le_bytes());
134        h[60..68].copy_from_slice(&10.0f64.to_le_bytes());
135
136        let parsed = parse(&h).unwrap();
137        assert_eq!(parsed.file_length_words, 500);
138        assert_eq!(parsed.shape_type, ShapeType::Polygon);
139        assert_eq!(parsed.bbox_xy, [0.0, 0.0, 10.0, 10.0]);
140    }
141
142    #[test]
143    fn bad_magic_errors() {
144        let mut h = vec![0u8; 100];
145        h[0..4].copy_from_slice(&1234i32.to_be_bytes());
146        h[28..32].copy_from_slice(&SHP_VERSION.to_le_bytes());
147        assert!(parse(&h).is_err());
148    }
149
150    #[test]
151    fn short_input_errors() {
152        let h = vec![0u8; 50];
153        assert!(parse(&h).is_err());
154    }
155}