1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
//! DBF (.dbf) attribute file handling
//!
//! This module handles reading and writing dBase III/IV (.dbf) files,
//! which contain the attribute data for Shapefile features.
pub mod encoding;
pub mod memo;
pub mod record;
pub use encoding::{resolve_cpg, resolve_ldid};
pub use memo::{MemoError, MemoFile, MemoVersion};
pub use record::{FieldDescriptor, FieldType, FieldValue};
use ::encoding_rs::Encoding;
use crate::error::{Result, ShapefileError};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::collections::HashMap;
use std::io::{Read, Seek, Write};
use std::sync::atomic::{AtomicBool, Ordering};
/// DBF header size in bytes
pub const DBF_HEADER_SIZE: usize = 32;
/// DBF field descriptor size in bytes
pub const FIELD_DESCRIPTOR_SIZE: usize = 32;
/// DBF header terminator
pub const HEADER_TERMINATOR: u8 = 0x0D;
/// DBF record deletion marker (for deleted records)
pub const RECORD_DELETED: u8 = 0x2A; // '*'
/// DBF record active marker (for active records)
pub const RECORD_ACTIVE: u8 = 0x20; // ' '
/// DBF file terminator
pub const FILE_TERMINATOR: u8 = 0x1A;
/// DBF header
#[derive(Debug, Clone)]
pub struct DbfHeader {
/// Version (3 for dBase III, 4 for dBase IV)
pub version: u8,
/// Last update year (YY, e.g., 24 for 2024)
pub year: u8,
/// Last update month (1-12)
pub month: u8,
/// Last update day (1-31)
pub day: u8,
/// Number of records
pub record_count: u32,
/// Header size in bytes (including field descriptors)
pub header_size: u16,
/// Record size in bytes
pub record_size: u16,
/// Code page (for character encoding)
pub code_page: u8,
}
impl DbfHeader {
/// Creates a new DBF header
pub fn new(record_count: u32, field_descriptors: &[FieldDescriptor]) -> Result<Self> {
// Calculate record size (1 byte for deletion flag + sum of field lengths)
let record_size: usize = 1 + field_descriptors
.iter()
.map(|f| f.length as usize)
.sum::<usize>();
// Calculate header size (32 bytes header + field descriptors + terminator)
let header_size = DBF_HEADER_SIZE + (field_descriptors.len() * FIELD_DESCRIPTOR_SIZE) + 1;
// Get current date
let now = std::time::SystemTime::now();
let duration = now.duration_since(std::time::UNIX_EPOCH).map_err(|_| {
ShapefileError::InvalidDbfHeader {
message: "failed to get current time".to_string(),
}
})?;
// Simple date calculation (approximation)
let days_since_epoch = duration.as_secs() / 86400;
let year = ((days_since_epoch / 365) % 100) as u8; // Last 2 digits
let month = 1; // Default to January
let day = 1; // Default to 1st
Ok(Self {
version: 3, // dBase III
year,
month,
day,
record_count,
header_size: header_size as u16,
record_size: record_size as u16,
code_page: 0, // No specific code page
})
}
/// Reads a DBF header from a reader
pub fn read<R: Read>(reader: &mut R) -> Result<Self> {
// Read version (1 byte)
let mut version = [0u8; 1];
reader
.read_exact(&mut version)
.map_err(|_| ShapefileError::unexpected_eof("reading dbf version"))?;
// Read last update date (3 bytes: YY, MM, DD)
let mut date = [0u8; 3];
reader
.read_exact(&mut date)
.map_err(|_| ShapefileError::unexpected_eof("reading dbf date"))?;
// Read record count (4 bytes, little endian)
let record_count = reader
.read_u32::<LittleEndian>()
.map_err(|_| ShapefileError::unexpected_eof("reading record count"))?;
// Read header size (2 bytes, little endian)
let header_size = reader
.read_u16::<LittleEndian>()
.map_err(|_| ShapefileError::unexpected_eof("reading header size"))?;
// Read record size (2 bytes, little endian)
let record_size = reader
.read_u16::<LittleEndian>()
.map_err(|_| ShapefileError::unexpected_eof("reading record size"))?;
// Skip reserved bytes (20 bytes)
let mut reserved = [0u8; 20];
reader
.read_exact(&mut reserved)
.map_err(|_| ShapefileError::unexpected_eof("reading dbf reserved bytes"))?;
// Code page is at byte 29 (in the reserved area)
let code_page = reserved[19];
Ok(Self {
version: version[0],
year: date[0],
month: date[1],
day: date[2],
record_count,
header_size,
record_size,
code_page,
})
}
/// Writes a DBF header to a writer
pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
// Write version (1 byte)
writer
.write_all(&[self.version])
.map_err(ShapefileError::Io)?;
// Write last update date (3 bytes)
writer
.write_all(&[self.year, self.month, self.day])
.map_err(ShapefileError::Io)?;
// Write record count (4 bytes, little endian)
writer
.write_u32::<LittleEndian>(self.record_count)
.map_err(ShapefileError::Io)?;
// Write header size (2 bytes, little endian)
writer
.write_u16::<LittleEndian>(self.header_size)
.map_err(ShapefileError::Io)?;
// Write record size (2 bytes, little endian)
writer
.write_u16::<LittleEndian>(self.record_size)
.map_err(ShapefileError::Io)?;
// Write reserved bytes (20 bytes, with code page at position 19)
let mut reserved = [0u8; 20];
reserved[19] = self.code_page;
writer.write_all(&reserved).map_err(ShapefileError::Io)?;
Ok(())
}
}
/// A DBF record (row of attribute data)
#[derive(Debug, Clone)]
pub struct DbfRecord {
/// Field values (in order)
pub values: Vec<FieldValue>,
/// Whether this record is deleted
pub deleted: bool,
}
impl DbfRecord {
/// Creates a new DBF record
pub fn new(values: Vec<FieldValue>) -> Self {
Self {
values,
deleted: false,
}
}
/// Reads a DBF record from a reader, decoding text as UTF-8 (lossy).
pub fn read<R: Read>(reader: &mut R, field_descriptors: &[FieldDescriptor]) -> Result<Self> {
Self::read_with_encoding(reader, field_descriptors, encoding::DEFAULT)
}
/// Reads a DBF record from a reader, transcoding text fields via `encoding`.
pub fn read_with_encoding<R: Read>(
reader: &mut R,
field_descriptors: &[FieldDescriptor],
encoding: &'static Encoding,
) -> Result<Self> {
// Read deletion marker (1 byte)
let mut marker = [0u8; 1];
reader
.read_exact(&mut marker)
.map_err(|_| ShapefileError::unexpected_eof("reading record marker"))?;
let deleted = marker[0] == RECORD_DELETED;
// Read field values
let mut values = Vec::with_capacity(field_descriptors.len());
for field in field_descriptors {
let mut field_bytes = vec![0u8; field.length as usize];
reader
.read_exact(&mut field_bytes)
.map_err(|_| ShapefileError::unexpected_eof("reading field value"))?;
let value = FieldValue::parse_with_encoding(
&field_bytes,
field.field_type,
field.decimal_count,
encoding,
)?;
values.push(value);
}
Ok(Self { values, deleted })
}
/// Writes a DBF record to a writer
pub fn write<W: Write>(
&self,
writer: &mut W,
field_descriptors: &[FieldDescriptor],
) -> Result<()> {
// Write deletion marker
let marker = if self.deleted {
RECORD_DELETED
} else {
RECORD_ACTIVE
};
writer.write_all(&[marker]).map_err(ShapefileError::Io)?;
// Write field values
if self.values.len() != field_descriptors.len() {
return Err(ShapefileError::DbfError {
message: format!(
"value count mismatch: expected {}, got {}",
field_descriptors.len(),
self.values.len()
),
field: None,
record: None,
});
}
for (value, field) in self.values.iter().zip(field_descriptors) {
let field_bytes = value.format(field.length as usize);
writer.write_all(&field_bytes).map_err(ShapefileError::Io)?;
}
Ok(())
}
/// Returns values as a HashMap (field name -> value)
pub fn to_map(&self, field_descriptors: &[FieldDescriptor]) -> HashMap<String, FieldValue> {
field_descriptors
.iter()
.zip(&self.values)
.map(|(field, value)| (field.name.clone(), value.clone()))
.collect()
}
}
/// DBF (.dbf) reader
pub struct DbfReader<R: Read> {
reader: R,
header: DbfHeader,
field_descriptors: Vec<FieldDescriptor>,
/// Optional sibling `.dbt` memo file. Populated by
/// [`DbfReader::set_memo_file`]; when present, memo-field values are
/// dereferenced from this file as part of [`DbfReader::read_record`].
memo: Option<MemoFile>,
/// One-shot guard: emit the "memo field without .dbt" warning at most
/// once per `DbfReader` to avoid flooding logs on large tables.
missing_memo_warned: AtomicBool,
/// Encoding used to transcode `Character`/memo fields. Defaults to the
/// table's header code page (LDID byte) or UTF-8; can be overridden via
/// [`DbfReader::set_encoding`] (e.g. from a sibling `.cpg`).
encoding: &'static Encoding,
}
impl<R: Read> DbfReader<R> {
/// Creates a new DBF reader
pub fn new(mut reader: R) -> Result<Self> {
// Read header
let header = DbfHeader::read(&mut reader)?;
// Calculate number of field descriptors.
//
// `header_size` is read verbatim from the (possibly truncated, corrupt,
// or attacker-controlled) file. It must be strictly larger than the
// fixed 32-byte header plus the 1-byte terminator, otherwise the
// subtraction below would underflow (panic in debug builds, wrap to a
// near-`usize::MAX` value in release builds and then request a
// multi-terabyte `Vec::with_capacity`). Guard with a checked subtraction.
let usable = (header.header_size as usize)
.checked_sub(DBF_HEADER_SIZE + 1)
.ok_or_else(|| ShapefileError::InvalidDbfHeader {
message: format!(
"header_size {} too small (must be > {})",
header.header_size,
DBF_HEADER_SIZE + 1
),
})?;
let num_fields = usable / FIELD_DESCRIPTOR_SIZE;
// Sanity bound: real DBF tables have at most a few thousand fields. A
// crafted (large-but-not-underflowing) `header_size` must not be able to
// request a huge `Vec::with_capacity` allocation.
const MAX_DBF_FIELDS: usize = 65_535;
if num_fields > MAX_DBF_FIELDS {
return Err(ShapefileError::InvalidDbfHeader {
message: format!("implausible DBF field count {num_fields} (max {MAX_DBF_FIELDS})"),
});
}
// Read field descriptors
let mut field_descriptors = Vec::with_capacity(num_fields);
for _ in 0..num_fields {
let descriptor = FieldDescriptor::read(&mut reader)?;
field_descriptors.push(descriptor);
}
// Read header terminator
let mut terminator = [0u8; 1];
reader
.read_exact(&mut terminator)
.map_err(|_| ShapefileError::unexpected_eof("reading header terminator"))?;
if terminator[0] != HEADER_TERMINATOR {
return Err(ShapefileError::InvalidDbfHeader {
message: format!(
"invalid header terminator: expected {}, got {}",
HEADER_TERMINATOR, terminator[0]
),
});
}
// Default encoding from the header's language-driver byte, else UTF-8.
let encoding = encoding::resolve_ldid(header.code_page).unwrap_or(encoding::DEFAULT);
Ok(Self {
reader,
header,
field_descriptors,
memo: None,
missing_memo_warned: AtomicBool::new(false),
encoding,
})
}
/// Returns the header
pub fn header(&self) -> &DbfHeader {
&self.header
}
/// Returns the encoding used to transcode `Character`/memo fields.
pub fn encoding(&self) -> &'static Encoding {
self.encoding
}
/// Overrides the encoding used to transcode `Character`/memo fields.
///
/// Propagates to any attached memo file so memo text uses the same code page.
pub fn set_encoding(&mut self, encoding: &'static Encoding) {
self.encoding = encoding;
if let Some(memo) = self.memo.as_mut() {
memo.set_encoding(encoding);
}
}
/// Returns the field descriptors
pub fn field_descriptors(&self) -> &[FieldDescriptor] {
&self.field_descriptors
}
/// Attaches a sibling `.dbt` memo file to this reader.
///
/// When a memo file is attached, subsequent calls to [`Self::read_record`]
/// will dereference any [`FieldType::Memo`] columns: the on-disk
/// 10-byte ASCII pointer is parsed as a block index and the corresponding
/// text is read from the memo file. If parsing or lookup fails the field
/// is returned as an empty string (`FieldValue::String(String::new())`).
///
/// Replaces any previously attached memo handle.
pub fn set_memo_file(&mut self, mut memo: MemoFile) {
memo.set_encoding(self.encoding);
self.memo = Some(memo);
}
/// Returns whether a sibling `.dbt` memo file is attached.
pub fn has_memo(&self) -> bool {
self.memo.is_some()
}
/// Returns whether this DBF table declares any Memo (`M`) columns.
fn has_memo_field(&self) -> bool {
self.field_descriptors
.iter()
.any(|f| f.field_type == FieldType::Memo)
}
/// Dereferences memo pointers in `record` against the attached memo file.
///
/// For each [`FieldType::Memo`] column whose value parses to a `u32`
/// block index, the parsed text replaces the in-place pointer string.
/// On parse failure the field is left as an empty string.
///
/// When the table declares memo fields but no `.dbt` is attached, a
/// single one-time `tracing::warn!` is emitted via
/// [`DbfReader::missing_memo_warned`] so consumers know the data is
/// incomplete without flooding the log.
fn resolve_memo_fields(&mut self, record: &mut DbfRecord) -> Result<()> {
if self.memo.is_none() {
if self.has_memo_field() && !self.missing_memo_warned.swap(true, Ordering::Relaxed) {
tracing::warn!(
"DBF Memo field but no .dbt attached; memo values will be returned as empty"
);
}
return Ok(());
}
// Snapshot the column types so we can mutate values while iterating.
let column_types: Vec<FieldType> = self
.field_descriptors
.iter()
.map(|f| f.field_type)
.collect();
for (idx, value) in record.values.iter_mut().enumerate() {
if column_types.get(idx).copied() != Some(FieldType::Memo) {
continue;
}
// Memo "pointer" is the 10-byte ASCII value already parsed into a
// string. Empty / unparseable pointers map to an empty memo.
let pointer_text = match value {
FieldValue::String(s) => s.trim().to_string(),
FieldValue::Null => String::new(),
_ => continue,
};
if pointer_text.is_empty() {
*value = FieldValue::String(String::new());
continue;
}
let block_index = match pointer_text.parse::<u32>() {
Ok(n) => n,
Err(_) => {
*value = FieldValue::String(String::new());
continue;
}
};
// SAFETY: we just confirmed `self.memo.is_some()` above; the
// closure cannot be `None` here.
if let Some(memo) = self.memo.as_mut() {
match memo.read_block(block_index) {
Ok(text) => *value = FieldValue::String(text),
Err(MemoError::BlockIndexOutOfRange { .. }) => {
// Out-of-range pointers are tolerated as empty memos
// so that one bad row does not abort the whole scan.
*value = FieldValue::String(String::new());
}
Err(MemoError::Io(e)) => return Err(ShapefileError::Io(e)),
Err(other) => {
return Err(ShapefileError::DbfError {
message: format!("memo lookup failed: {}", other),
field: None,
record: None,
});
}
}
}
}
Ok(())
}
/// Reads the next record
pub fn read_record(&mut self) -> Result<Option<DbfRecord>> {
let raw = match DbfRecord::read_with_encoding(
&mut self.reader,
&self.field_descriptors,
self.encoding,
) {
Ok(record) => record,
Err(ShapefileError::Io(ref e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
return Ok(None);
}
Err(ShapefileError::UnexpectedEof { .. }) => {
// EOF when reading record is expected at end of file
return Ok(None);
}
Err(e) => return Err(e),
};
let mut record = raw;
self.resolve_memo_fields(&mut record)?;
Ok(Some(record))
}
/// Reads all records
pub fn read_all_records(&mut self) -> Result<Vec<DbfRecord>> {
let mut records = Vec::with_capacity(self.header.record_count as usize);
while let Some(record) = self.read_record()? {
// Check for file terminator
if records.len() >= self.header.record_count as usize {
break;
}
records.push(record);
}
Ok(records)
}
}
/// DBF (.dbf) writer
pub struct DbfWriter<W: Write> {
writer: W,
header: DbfHeader,
field_descriptors: Vec<FieldDescriptor>,
record_count: u32,
}
impl<W: Write> DbfWriter<W> {
/// Creates a new DBF writer
pub fn new(writer: W, field_descriptors: Vec<FieldDescriptor>) -> Result<Self> {
let header = DbfHeader::new(0, &field_descriptors)?;
Ok(Self {
writer,
header,
field_descriptors,
record_count: 0,
})
}
/// Writes the header (should be called first)
pub fn write_header(&mut self) -> Result<()> {
// Update header with current record count
self.header.record_count = self.record_count;
self.header.write(&mut self.writer)?;
// Write field descriptors
for field in &self.field_descriptors {
field.write(&mut self.writer)?;
}
// Write header terminator
self.writer
.write_all(&[HEADER_TERMINATOR])
.map_err(ShapefileError::Io)?;
Ok(())
}
/// Writes a record
pub fn write_record(&mut self, record: &DbfRecord) -> Result<()> {
record.write(&mut self.writer, &self.field_descriptors)?;
self.record_count += 1;
Ok(())
}
/// Flushes the internal writer
pub fn flush(&mut self) -> Result<()> {
self.writer.flush().map_err(ShapefileError::Io)
}
/// Finalizes the file (updates header with record count and writes terminator)
pub fn finalize(mut self) -> Result<()> {
// Write file terminator
self.writer
.write_all(&[FILE_TERMINATOR])
.map_err(ShapefileError::Io)?;
Ok(())
}
}
impl<W: Write + Seek> DbfWriter<W> {
/// Updates the record count in the header (for seekable writers)
pub fn update_record_count(&mut self) -> Result<()> {
use byteorder::WriteBytesExt;
// Update header record count
self.header.record_count = self.record_count;
// Seek to record count position in header (byte 4)
self.writer
.seek(std::io::SeekFrom::Start(4))
.map_err(ShapefileError::Io)?;
// Write record count (little endian)
self.writer
.write_u32::<LittleEndian>(self.record_count)
.map_err(ShapefileError::Io)?;
// Flush to ensure the update is written
self.writer.flush().map_err(ShapefileError::Io)?;
// Seek back to end of file
self.writer
.seek(std::io::SeekFrom::End(0))
.map_err(ShapefileError::Io)?;
Ok(())
}
}
impl DbfWriter<std::fs::File> {
/// Syncs all data to disk (only available for File writers)
pub fn sync_all(&mut self) -> Result<()> {
self.writer.sync_all().map_err(ShapefileError::Io)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_dbf_header_round_trip() {
let fields = vec![
FieldDescriptor::new("NAME".to_string(), FieldType::Character, 50, 0)
.expect("valid NAME field descriptor"),
FieldDescriptor::new("VALUE".to_string(), FieldType::Number, 10, 2)
.expect("valid VALUE field descriptor"),
];
let header = DbfHeader::new(10, &fields).expect("valid dbf header");
let mut buffer = Vec::new();
header.write(&mut buffer).expect("write dbf header");
assert_eq!(buffer.len(), DBF_HEADER_SIZE);
let mut cursor = Cursor::new(buffer);
let read_header = DbfHeader::read(&mut cursor).expect("read dbf header");
assert_eq!(read_header.version, 3);
assert_eq!(read_header.record_count, 10);
}
#[test]
fn test_dbf_record_round_trip() {
let fields = vec![
FieldDescriptor::new("NAME".to_string(), FieldType::Character, 10, 0)
.expect("valid NAME field descriptor"),
FieldDescriptor::new("AGE".to_string(), FieldType::Number, 3, 0)
.expect("valid AGE field descriptor"),
];
let record = DbfRecord::new(vec![
FieldValue::String("Alice".to_string()),
FieldValue::Integer(30),
]);
let mut buffer = Vec::new();
record
.write(&mut buffer, &fields)
.expect("write dbf record");
let mut cursor = Cursor::new(buffer);
let read_record = DbfRecord::read(&mut cursor, &fields).expect("read dbf record");
assert!(!read_record.deleted);
assert_eq!(read_record.values.len(), 2);
}
#[test]
fn test_dbf_reader_writer() {
let fields = vec![
FieldDescriptor::new("NAME".to_string(), FieldType::Character, 20, 0)
.expect("valid field"),
FieldDescriptor::new("VALUE".to_string(), FieldType::Number, 10, 2)
.expect("valid field"),
];
let mut buffer = Cursor::new(Vec::new());
// Collect records and write
let records = vec![
DbfRecord::new(vec![
FieldValue::String("Test1".to_string()),
FieldValue::Float(123.45),
]),
DbfRecord::new(vec![
FieldValue::String("Test2".to_string()),
FieldValue::Float(678.90),
]),
];
// Create header with known record count
let header = DbfHeader::new(records.len() as u32, &fields).expect("valid header");
// Write header
header.write(&mut buffer).expect("write header");
// Write field descriptors
for field in &fields {
field.write(&mut buffer).expect("write field");
}
buffer
.write_all(&[HEADER_TERMINATOR])
.expect("write terminator");
// Write records
for record in &records {
record.write(&mut buffer, &fields).expect("write record");
}
// Write terminator
buffer.write_all(&[FILE_TERMINATOR]).expect("write EOF");
// Read
buffer.set_position(0);
let mut reader = DbfReader::new(buffer).expect("create reader");
assert_eq!(reader.field_descriptors().len(), 2);
// Check buffer length
let expected_record_size = 1 + 20 + 10; // deletion flag + NAME field + VALUE field
let _expected_size =
DBF_HEADER_SIZE + (2 * FIELD_DESCRIPTOR_SIZE) + 1 + (2 * expected_record_size) + 1;
let read_records = reader.read_all_records().expect("read records");
assert_eq!(read_records.len(), 2);
}
/// Builds a raw 32-byte DBF header whose `header_size` field is set to the
/// given value, with just enough trailing bytes for `DbfReader::new` to try
/// to proceed. Used to exercise the `header_size` underflow guard.
fn raw_dbf_with_header_size(header_size: u16) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(3u8); // version
buf.extend_from_slice(&[0u8, 0u8, 0u8]); // date YY/MM/DD
buf.extend_from_slice(&0u32.to_le_bytes()); // record_count
buf.extend_from_slice(&header_size.to_le_bytes()); // header_size
buf.extend_from_slice(&1u16.to_le_bytes()); // record_size
buf.extend_from_slice(&[0u8; 20]); // reserved
// A few extra bytes so any (incorrect) field-descriptor read would have
// data to consume; a correct implementation errors before reaching here.
buf.extend_from_slice(&[0u8; 8]);
buf
}
/// Regression: a DBF file whose `header_size` is `<= DBF_HEADER_SIZE + 1`
/// must return `InvalidDbfHeader` rather than panic (debug) or attempt a
/// huge allocation (release) via `usize` subtraction underflow.
#[test]
fn test_dbf_header_size_zero_no_underflow() {
for header_size in [0u16, 1, 16, 32, 33] {
let raw = raw_dbf_with_header_size(header_size);
let cursor = Cursor::new(raw);
let result = DbfReader::new(cursor);
assert!(
matches!(result, Err(ShapefileError::InvalidDbfHeader { .. })),
"header_size {header_size} must yield InvalidDbfHeader, got {:?}",
result.map(|_| "Ok").map_err(|e| e.to_string())
);
}
}
/// A `header_size` just past the terminator boundary (34 = 32 + 1 field
/// worth would be 1) still parses structurally; here 65 encodes exactly one
/// 32-byte field descriptor, confirming the guard does not over-reject.
#[test]
fn test_dbf_header_size_valid_boundary_accepts() {
// 32 (header) + 32 (one field) + 1 (terminator) = 65
let fields = vec![
FieldDescriptor::new("NAME".to_string(), FieldType::Character, 10, 0)
.expect("valid field"),
];
let header = DbfHeader::new(0, &fields).expect("valid header");
let mut buffer = Cursor::new(Vec::new());
header.write(&mut buffer).expect("write header");
for field in &fields {
field.write(&mut buffer).expect("write field");
}
buffer
.write_all(&[HEADER_TERMINATOR])
.expect("write terminator");
buffer.write_all(&[FILE_TERMINATOR]).expect("write EOF");
buffer.set_position(0);
let reader = DbfReader::new(buffer).expect("valid single-field DBF must open");
assert_eq!(reader.field_descriptors().len(), 1);
}
}