Skip to main content

oxigdal_shapefile/shx/
mod.rs

1//! Shapefile index (.shx) file handling
2//!
3//! This module handles reading and writing the Shapefile index (.shx) file,
4//! which contains the offset and length of each record in the .shp file.
5//!
6//! The .shx file has the same header as the .shp file, followed by pairs of
7//! integers (offset and content length in 16-bit words) for each record.
8
9use crate::error::{Result, ShapefileError};
10use crate::shp::header::{BoundingBox, HEADER_SIZE, ShapefileHeader};
11use crate::shp::shapes::ShapeType;
12use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
13use std::io::{Read, Seek, SeekFrom, Write};
14
15/// Index entry size in bytes (offset + content length)
16pub const INDEX_ENTRY_SIZE: usize = 8;
17
18/// Index entry (offset and length of a record in the .shp file)
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct IndexEntry {
21    /// Offset in 16-bit words from the start of the .shp file
22    pub offset: i32,
23    /// Content length in 16-bit words
24    pub content_length: i32,
25}
26
27impl IndexEntry {
28    /// Creates a new index entry
29    pub fn new(offset: i32, content_length: i32) -> Self {
30        Self {
31            offset,
32            content_length,
33        }
34    }
35
36    /// Reads an index entry from a reader
37    pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
38        let offset = reader
39            .read_i32::<BigEndian>()
40            .map_err(|_| ShapefileError::unexpected_eof("reading index offset"))?;
41
42        let content_length = reader
43            .read_i32::<BigEndian>()
44            .map_err(|_| ShapefileError::unexpected_eof("reading index content length"))?;
45
46        Ok(Self {
47            offset,
48            content_length,
49        })
50    }
51
52    /// Writes an index entry to a writer
53    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
54        writer
55            .write_i32::<BigEndian>(self.offset)
56            .map_err(ShapefileError::Io)?;
57
58        writer
59            .write_i32::<BigEndian>(self.content_length)
60            .map_err(ShapefileError::Io)?;
61
62        Ok(())
63    }
64}
65
66/// Shapefile index (.shx) reader
67pub struct ShxReader<R: Read> {
68    reader: R,
69    header: ShapefileHeader,
70}
71
72impl<R: Read> ShxReader<R> {
73    /// Creates a new Shapefile index reader
74    pub fn new(mut reader: R) -> Result<Self> {
75        let header = ShapefileHeader::read(&mut reader)?;
76        Ok(Self { reader, header })
77    }
78
79    /// Returns the header
80    pub fn header(&self) -> &ShapefileHeader {
81        &self.header
82    }
83
84    /// Reads all index entries
85    pub fn read_all_entries(&mut self) -> Result<Vec<IndexEntry>> {
86        let mut entries = Vec::new();
87
88        loop {
89            match IndexEntry::read(&mut self.reader) {
90                Ok(entry) => entries.push(entry),
91                Err(ShapefileError::UnexpectedEof { .. }) => {
92                    // Expected EOF when we've read all entries
93                    break;
94                }
95                Err(ShapefileError::Io(ref e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
96                    break;
97                }
98                Err(e) => return Err(e),
99            }
100        }
101
102        Ok(entries)
103    }
104
105    /// Calculates the number of records from file length
106    pub fn record_count(&self) -> usize {
107        // File length is in 16-bit words, header is 100 bytes (50 words)
108        // Each index entry is 8 bytes (4 words)
109        let file_length_bytes = (self.header.file_length as usize) * 2;
110        if file_length_bytes < HEADER_SIZE {
111            return 0;
112        }
113        (file_length_bytes - HEADER_SIZE) / INDEX_ENTRY_SIZE
114    }
115}
116
117/// Shapefile index (.shx) writer
118pub struct ShxWriter<W: Write> {
119    writer: W,
120    header: ShapefileHeader,
121    entries: Vec<IndexEntry>,
122}
123
124impl<W: Write> ShxWriter<W> {
125    /// Creates a new Shapefile index writer
126    pub fn new(writer: W, shape_type: ShapeType, bbox: BoundingBox) -> Self {
127        let header = ShapefileHeader::new(shape_type, bbox);
128        Self {
129            writer,
130            header,
131            entries: Vec::new(),
132        }
133    }
134
135    /// Adds an index entry
136    pub fn add_entry(&mut self, offset: i32, content_length: i32) {
137        self.entries.push(IndexEntry::new(offset, content_length));
138    }
139
140    /// Writes the header and all entries
141    pub fn write_all(&mut self) -> Result<()> {
142        // Update file length in header
143        // File length in 16-bit words = header (50 words) + entries (4 words each)
144        self.header.file_length = 50 + (self.entries.len() as i32 * 4);
145
146        // Write header
147        self.header.write(&mut self.writer)?;
148
149        // Write all entries
150        for entry in &self.entries {
151            entry.write(&mut self.writer)?;
152        }
153
154        Ok(())
155    }
156
157    /// Flushes the internal writer to ensure all data is written
158    pub fn flush(&mut self) -> Result<()> {
159        self.writer.flush().map_err(ShapefileError::Io)
160    }
161}
162
163impl<W: Write + Seek> ShxWriter<W> {
164    /// Updates the file length in the header (for seekable writers)
165    pub fn update_file_length(&mut self) -> Result<()> {
166        // Calculate file length in 16-bit words
167        self.header.file_length = 50 + (self.entries.len() as i32 * 4);
168
169        // Seek to file length position in header (byte 24)
170        self.writer
171            .seek(SeekFrom::Start(24))
172            .map_err(ShapefileError::Io)?;
173
174        // Write file length (big endian)
175        self.writer
176            .write_i32::<BigEndian>(self.header.file_length)
177            .map_err(ShapefileError::Io)?;
178
179        Ok(())
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use std::io::Cursor;
187
188    #[test]
189    fn test_index_entry_round_trip() {
190        let entry = IndexEntry::new(50, 100);
191
192        let mut buffer = Vec::new();
193        entry.write(&mut buffer).expect("write index entry");
194
195        assert_eq!(buffer.len(), INDEX_ENTRY_SIZE);
196
197        let mut cursor = Cursor::new(buffer);
198        let read_entry = IndexEntry::read(&mut cursor).expect("read index entry");
199
200        assert_eq!(read_entry, entry);
201    }
202
203    #[test]
204    fn test_shx_reader_writer() {
205        let bbox = BoundingBox::new_2d(-180.0, -90.0, 180.0, 90.0).expect("valid bbox");
206        let mut buffer = Cursor::new(Vec::new());
207
208        // Write
209        {
210            let mut writer = ShxWriter::new(&mut buffer, ShapeType::Point, bbox);
211            writer.add_entry(50, 10); // First record at offset 50, length 10
212            writer.add_entry(60, 10); // Second record at offset 60, length 10
213            writer.add_entry(70, 10); // Third record at offset 70, length 10
214            writer.write_all().expect("write all shx entries");
215        }
216
217        // Read
218        buffer.set_position(0);
219        let mut reader = ShxReader::new(buffer).expect("create shx reader");
220
221        assert_eq!(reader.header().shape_type, ShapeType::Point);
222        assert_eq!(reader.record_count(), 3);
223
224        let entries = reader.read_all_entries().expect("read all shx entries");
225        assert_eq!(entries.len(), 3);
226        assert_eq!(entries[0].offset, 50);
227        assert_eq!(entries[0].content_length, 10);
228        assert_eq!(entries[1].offset, 60);
229        assert_eq!(entries[2].offset, 70);
230    }
231
232    #[test]
233    fn test_shx_file_length_calculation() {
234        let bbox = BoundingBox::new_2d(-180.0, -90.0, 180.0, 90.0)
235            .expect("valid bbox for shx file length test");
236        let mut buffer = Vec::new();
237
238        let mut writer = ShxWriter::new(&mut buffer, ShapeType::Point, bbox);
239        writer.add_entry(50, 10);
240        writer.add_entry(60, 10);
241
242        // Before writing, calculate expected file length
243        // Header: 50 words, 2 entries: 2 * 4 = 8 words, Total: 58 words
244        writer
245            .write_all()
246            .expect("write all for file length calculation");
247
248        let cursor = Cursor::new(buffer);
249        let reader = ShxReader::new(cursor).expect("create reader for file length check");
250        assert_eq!(reader.header().file_length, 58);
251    }
252
253    #[test]
254    fn test_seekable_update() {
255        let bbox = BoundingBox::new_2d(-180.0, -90.0, 180.0, 90.0)
256            .expect("valid bbox for seekable update test");
257        let mut buffer = Cursor::new(Vec::new());
258
259        let mut writer = ShxWriter::new(&mut buffer, ShapeType::Point, bbox);
260        writer.add_entry(50, 10);
261        writer.add_entry(60, 10);
262        writer.write_all().expect("write all for seekable update");
263
264        // Update file length
265        writer.update_file_length().expect("update file length");
266
267        // Verify
268        buffer.set_position(0);
269        let reader = ShxReader::new(buffer).expect("create reader after seekable update");
270        assert_eq!(reader.header().file_length, 58);
271    }
272}