oxigdal_shapefile/dbf/
memo.rs1use std::fs::File;
36use std::io::{self, Read, Seek, SeekFrom};
37use std::path::Path;
38
39#[derive(Debug, thiserror::Error)]
41pub enum MemoError {
42 #[error("I/O error: {0}")]
44 Io(#[from] io::Error),
45 #[error("Invalid memo file header: {0}")]
47 InvalidHeader(String),
48 #[error("Unsupported memo version: {0}")]
50 UnsupportedVersion(String),
51 #[error("Memo block index {index} out of range (available={available})")]
53 BlockIndexOutOfRange {
54 index: u32,
56 available: u32,
58 },
59 #[error("Missing memo block terminator (expected 0x1A 0x1A)")]
61 MissingTerminator,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum MemoVersion {
67 DBase3,
69 DBase4,
71 FoxPro,
73}
74
75const TERMINATOR_SEARCH_BOUND: usize = 1_000_000;
80
81const DBASE4_ENTRY_MAGIC: [u8; 4] = [0xFF, 0xFF, 0x08, 0x00];
83
84const DBASE4_ENTRY_HEADER_LEN: usize = 8;
86
87const DEFAULT_BLOCK_SIZE: u32 = 512;
89
90pub struct MemoFile {
95 handle: File,
96 block_size: u32,
97 version: MemoVersion,
98 next_block: u32,
99 encoding: &'static encoding_rs::Encoding,
101}
102
103impl std::fmt::Debug for MemoFile {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("MemoFile")
106 .field("block_size", &self.block_size)
107 .field("version", &self.version)
108 .field("next_block", &self.next_block)
109 .finish_non_exhaustive()
110 }
111}
112
113impl MemoFile {
114 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, MemoError> {
119 let mut handle = File::open(path.as_ref())?;
120 let mut header = [0u8; 512];
121 handle.read_exact(&mut header)?;
122
123 let next_block_bytes: [u8; 4] = header[0..4]
124 .try_into()
125 .map_err(|_| MemoError::InvalidHeader("could not read next_block field".to_string()))?;
126 let next_block = u32::from_le_bytes(next_block_bytes);
127
128 let version_byte = header[16];
129 let version = match version_byte {
130 0x03 => MemoVersion::DBase4,
131 0x00 => {
132 return Err(MemoError::UnsupportedVersion(
133 "dBase III memo files not supported (use dBase IV format)".to_string(),
134 ));
135 }
136 other => {
137 return Err(MemoError::UnsupportedVersion(format!(
138 "Unknown memo version byte 0x{:02X}",
139 other
140 )));
141 }
142 };
143
144 let block_size_bytes: [u8; 2] = header[20..22]
147 .try_into()
148 .map_err(|_| MemoError::InvalidHeader("could not read block_size field".to_string()))?;
149 let block_size_raw = u16::from_le_bytes(block_size_bytes) as u32;
150 let block_size = if block_size_raw == 0 {
151 DEFAULT_BLOCK_SIZE
152 } else {
153 block_size_raw
154 };
155
156 Ok(Self {
157 handle,
158 block_size,
159 version,
160 next_block,
161 encoding: encoding_rs::UTF_8,
162 })
163 }
164
165 pub fn set_encoding(&mut self, encoding: &'static encoding_rs::Encoding) {
167 self.encoding = encoding;
168 }
169
170 pub fn block_size(&self) -> u32 {
172 self.block_size
173 }
174
175 pub fn version(&self) -> MemoVersion {
177 self.version
178 }
179
180 pub fn next_block(&self) -> u32 {
183 self.next_block
184 }
185
186 pub fn read_block(&mut self, index: u32) -> Result<String, MemoError> {
190 if index >= self.next_block {
191 return Err(MemoError::BlockIndexOutOfRange {
192 index,
193 available: self.next_block,
194 });
195 }
196
197 let offset = u64::from(index) * u64::from(self.block_size);
198 self.handle.seek(SeekFrom::Start(offset))?;
199
200 let mut hdr = [0u8; DBASE4_ENTRY_HEADER_LEN];
205 self.handle.read_exact(&mut hdr)?;
206
207 if hdr[0..4] != DBASE4_ENTRY_MAGIC {
208 self.handle.seek(SeekFrom::Start(offset))?;
209 return self.read_block_terminator_search();
210 }
211
212 let length_bytes: [u8; 4] = hdr[4..8].try_into().map_err(|_| {
213 MemoError::InvalidHeader("could not read entry length field".to_string())
214 })?;
215 let length = u32::from_le_bytes(length_bytes) as usize;
216
217 let payload_len = length.saturating_sub(DBASE4_ENTRY_HEADER_LEN);
221 let mut payload = vec![0u8; payload_len];
222 self.handle.read_exact(&mut payload)?;
223
224 while payload.ends_with(&[0x1A]) {
227 payload.pop();
228 }
229
230 Ok(self
231 .encoding
232 .decode_without_bom_handling(&payload)
233 .0
234 .into_owned())
235 }
236
237 fn read_block_terminator_search(&mut self) -> Result<String, MemoError> {
240 let mut buf = Vec::new();
241 let mut chunk = [0u8; 512];
242 loop {
243 let n = self.handle.read(&mut chunk)?;
244 if n == 0 {
245 return Err(MemoError::MissingTerminator);
246 }
247 buf.extend_from_slice(&chunk[..n]);
248
249 if let Some(pos) = buf.windows(2).position(|w| w == [0x1A, 0x1A]) {
250 buf.truncate(pos);
251 return Ok(self
252 .encoding
253 .decode_without_bom_handling(&buf)
254 .0
255 .into_owned());
256 }
257
258 if buf.len() > TERMINATOR_SEARCH_BOUND {
259 return Err(MemoError::MissingTerminator);
260 }
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use std::io::Write;
269
270 fn build_dbase4_fixture(path: &Path, blocks: &[&str]) -> io::Result<()> {
272 let mut file = File::create(path)?;
273
274 let mut header = [0u8; 512];
277 let next_block = (blocks.len() as u32) + 1;
278 header[0..4].copy_from_slice(&next_block.to_le_bytes());
279 header[16] = 0x03; header[20..22].copy_from_slice(&512u16.to_le_bytes());
281 file.write_all(&header)?;
282
283 for text in blocks {
284 let mut block = vec![0u8; 512];
285 block[0..4].copy_from_slice(&DBASE4_ENTRY_MAGIC);
286 let length = (DBASE4_ENTRY_HEADER_LEN + text.len() + 2) as u32;
287 block[4..8].copy_from_slice(&length.to_le_bytes());
288 let text_bytes = text.as_bytes();
289 block[8..8 + text_bytes.len()].copy_from_slice(text_bytes);
290 block[8 + text_bytes.len()] = 0x1A;
291 block[8 + text_bytes.len() + 1] = 0x1A;
292 file.write_all(&block)?;
293 }
294
295 Ok(())
296 }
297
298 #[test]
299 fn test_memo_internal_open_and_read() {
300 let path = std::env::temp_dir().join("oxigdal_memo_internal_open.dbt");
301 build_dbase4_fixture(&path, &["Hello", "World"]).expect("build fixture");
302 let mut memo = MemoFile::open(&path).expect("open memo");
303 assert_eq!(memo.version(), MemoVersion::DBase4);
304 assert_eq!(memo.block_size(), 512);
305 assert_eq!(memo.read_block(1).expect("read block 1"), "Hello");
306 assert_eq!(memo.read_block(2).expect("read block 2"), "World");
307 let _ = std::fs::remove_file(&path);
308 }
309}