├── .gitignore ├── tests └── fixtures │ ├── people.dbf │ ├── dbase_83.dbf │ ├── dbase_83.dbt │ ├── dbase_8b.dbf │ ├── dbase_8b.dbt │ ├── dbase_03_cyrillic.dbf │ ├── dbase_83_missing_memo.dbf │ ├── dbase_83_missing_memo_record_0.yml │ ├── dbase_83_schema_ar.txt │ ├── dbase_83_schema_sq.txt │ ├── dbase_83_schema_sq_lim.txt │ ├── dbase_83_record_0.yml │ ├── dbase_83_record_9.yml │ ├── dbase_83_summary.txt │ └── dbase_03.dbf ├── Cargo.toml ├── src ├── main.rs └── dbf.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /tests/fixtures/people.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/people.dbf -------------------------------------------------------------------------------- /tests/fixtures/dbase_83.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/dbase_83.dbf -------------------------------------------------------------------------------- /tests/fixtures/dbase_83.dbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/dbase_83.dbt -------------------------------------------------------------------------------- /tests/fixtures/dbase_8b.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/dbase_8b.dbf -------------------------------------------------------------------------------- /tests/fixtures/dbase_8b.dbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/dbase_8b.dbt -------------------------------------------------------------------------------- /tests/fixtures/dbase_03_cyrillic.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/dbase_03_cyrillic.dbf -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_missing_memo.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcoury/dbf/master/tests/fixtures/dbase_83_missing_memo.dbf -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yawl" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.87" 8 | byteorder = "1.5.0" 9 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_missing_memo_record_0.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 87 3 | - 2 4 | - 0 5 | - 0 6 | - 87 7 | - '1' 8 | - Assorted Petits Fours 9 | - graphics/00000001/t_1.jpg 10 | - graphics/00000001/1.jpg 11 | - 0.0 12 | - 0.0 13 | - 14 | - 5.51 15 | - true 16 | - true 17 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use dbf::File; 2 | 3 | mod dbf; 4 | 5 | fn main() -> anyhow::Result<()> { 6 | let filename = std::env::args().nth(1).expect("No filename provided"); 7 | let dbf = File::open(&filename)?; 8 | 9 | println!("version: {:?}", dbf.header.file_type); 10 | 11 | for field in &dbf.fields { 12 | println!("{:?}", field); 13 | } 14 | 15 | for record in dbf { 16 | println!("{:?}", record?); 17 | } 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anyhow" 7 | version = "1.0.87" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8" 10 | 11 | [[package]] 12 | name = "byteorder" 13 | version = "1.5.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 16 | 17 | [[package]] 18 | name = "yawl" 19 | version = "0.1.0" 20 | dependencies = [ 21 | "anyhow", 22 | "byteorder", 23 | ] 24 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_schema_ar.txt: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define do 2 | create_table "dbase_83" do |t| 3 | t.column "id", :integer 4 | t.column "catcount", :integer 5 | t.column "agrpcount", :integer 6 | t.column "pgrpcount", :integer 7 | t.column "order", :integer 8 | t.column "code", :string, :limit => 50 9 | t.column "name", :string, :limit => 100 10 | t.column "thumbnail", :string, :limit => 254 11 | t.column "image", :string, :limit => 254 12 | t.column "price", :float 13 | t.column "cost", :float 14 | t.column "desc", :text 15 | t.column "weight", :float 16 | t.column "taxable", :boolean 17 | t.column "active", :boolean 18 | end 19 | end -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_schema_sq.txt: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | create_table(:dbase_83) do 4 | column :id, :integer 5 | column :catcount, :integer 6 | column :agrpcount, :integer 7 | column :pgrpcount, :integer 8 | column :order, :integer 9 | column :code, :varchar, :size => 50 10 | column :name, :varchar, :size => 100 11 | column :thumbnail, :varchar, :size => 254 12 | column :image, :varchar, :size => 254 13 | column :price, :float 14 | column :cost, :float 15 | column :desc, :text 16 | column :weight, :float 17 | column :taxable, :boolean 18 | column :active, :boolean 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_schema_sq_lim.txt: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | create_table(:dbase_83) do 4 | column :id, :integer 5 | column :catcount, :integer 6 | column :agrpcount, :integer 7 | column :pgrpcount, :integer 8 | column :order, :integer 9 | column :code, :varchar, :size => 50 10 | column :name, :varchar, :size => 100 11 | column :thumbnail, :varchar, :size => 254 12 | column :image, :varchar, :size => 254 13 | column :price, :float 14 | column :cost, :float 15 | column :desc, :text 16 | column :weight, :float 17 | column :taxable, :boolean 18 | column :active, :boolean 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_record_0.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 87 3 | - 2 4 | - 0 5 | - 0 6 | - 87 7 | - '1' 8 | - Assorted Petits Fours 9 | - graphics/00000001/t_1.jpg 10 | - graphics/00000001/1.jpg 11 | - 0.0 12 | - 0.0 13 | - "Our Original assortment...a little taste of heaven for everyone. Let us\r\nselect a special assortment of our chocolate and pastel favorites for you.\r\nEach petit four is its own special hand decorated creation. Multi-layers of\r\nmoist cake with combinations of specialty fillings create memorable cake\r\nconfections. Varietes include; Luscious Lemon, Strawberry Hearts, White\r\nChocolate, Mocha Bean, Roasted Almond, Triple Chocolate, Chocolate Hazelnut,\r\nGrand Orange, Plum Squares, Milk chocolate squares, and Raspberry Blanc." 14 | - 5.51 15 | - true 16 | - true 17 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_record_9.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - 34 3 | - 1 4 | - 0 5 | - 0 6 | - 34 7 | - AB01 8 | - Apricot Brandy Fruitcake 9 | - graphics/00000001/t_AB01.jpg 10 | - graphics/00000001/AB01.jpg 11 | - 37.95 12 | - 37.95 13 | - "Once tasted you will understand why we won The\r\nBoston Herald's Fruitcake Taste-off. 14 | Judges liked its generous size,\r\nluscious appearance, moist texture and fruit 15 | to cake ratio ... commented one\r\njudge \"It's a lip Smacker!\" Our signature fruitcake 16 | is baked with carefully\r\nselected ingredients that will be savored until the last 17 | moist crumb is\r\ndevoured each golden slice is brimming with Australian glaced 18 | apricots,\r\ntoasted pecans, candied orange peel, and currants, folded gently into 19 | a\r\nbrandy butter batter and slowly baked to perfection and then generously\r\nimbibed 20 | with \"Holiday Spirits\". Presented in a gift tin. (3lbs. 4oz)" 21 | - 0.0 22 | - false 23 | - true 24 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_83_summary.txt: -------------------------------------------------------------------------------- 1 | 2 | Database: dbase_83.dbf 3 | Type: (83) dBase III with memo file 4 | Memo File: true 5 | Records: 67 6 | 7 | Fields: 8 | Name Type Length Decimal 9 | ------------------------------------------------------------------------------ 10 | ID N 19 0 11 | CATCOUNT N 19 0 12 | AGRPCOUNT N 19 0 13 | PGRPCOUNT N 19 0 14 | ORDER N 19 0 15 | CODE C 50 0 16 | NAME C 100 0 17 | THUMBNAIL C 254 0 18 | IMAGE C 254 0 19 | PRICE N 13 2 20 | COST N 13 2 21 | DESC M 10 0 22 | WEIGHT N 13 2 23 | TAXABLE L 1 0 24 | ACTIVE L 1 0 25 | -------------------------------------------------------------------------------- /src/dbf.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use std::io::{BufRead, BufReader, Read, Seek}; 3 | 4 | use byteorder::{LittleEndian, ReadBytesExt}; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Header { 8 | pub file_type: FileType, 9 | pub has_memo: bool, 10 | pub last_update: (u8, u8, u8), 11 | pub num_records: u32, 12 | pub header_bytes: u16, 13 | pub record_bytes: u16, 14 | pub incomplete_tx: u8, 15 | pub encryption_flag: u8, 16 | pub mdx_flag: u8, 17 | pub language_driver_id: u8, 18 | } 19 | 20 | impl Header { 21 | pub fn read(reader: &mut R) -> anyhow::Result { 22 | let info = reader.read_u8()?; 23 | let file_type_id = info & 0b0000_0111; 24 | let has_memo = info & 0b1000_0000 != 0; 25 | 26 | let mut buffer = [0; 3]; 27 | reader.read_exact(&mut buffer)?; 28 | let year = buffer[0]; 29 | let month = buffer[1]; 30 | let day = buffer[2]; 31 | 32 | let num_records = reader.read_u32::()?; 33 | let header_bytes = reader.read_u16::()?; 34 | let record_bytes = reader.read_u16::()?; 35 | reader.seek(std::io::SeekFrom::Current(2))?; 36 | let incomplete_tx = reader.read_u8()?; 37 | let encryption_flag = reader.read_u8()?; 38 | reader.seek(std::io::SeekFrom::Current(12))?; 39 | let mdx_flag = reader.read_u8()?; 40 | let language_driver_id = reader.read_u8()?; 41 | reader.seek(std::io::SeekFrom::Current(2))?; 42 | 43 | let Some(file_type) = FileType::from_u8(file_type_id) else { 44 | anyhow::bail!("Unknown file type: {}", file_type_id); 45 | }; 46 | 47 | if file_type != FileType::DBase3Plus && file_type != FileType::DBase3PlusWithMemo { 48 | anyhow::bail!("Unsupported file type: {file_type_id} - {:?}", file_type); 49 | } 50 | 51 | Ok(Self { 52 | file_type, 53 | has_memo, 54 | last_update: (year, month, day), 55 | num_records, 56 | header_bytes, 57 | record_bytes, 58 | incomplete_tx, 59 | encryption_flag, 60 | mdx_flag, 61 | language_driver_id, 62 | }) 63 | } 64 | } 65 | 66 | #[derive(Debug, Clone, PartialEq)] 67 | #[repr(u8)] 68 | pub enum FileType { 69 | FoxBase = 0x02, 70 | DBase3Plus = 0x03, 71 | VisualFoxPro = 0x30, 72 | VisualFoxProAutoIncrement = 0x31, 73 | VisualFoxProVar = 0x32, 74 | DBase4SQLTable = 0x43, 75 | DBase4SQLSystem = 0x63, 76 | DBase3PlusWithMemo = 0x83, 77 | DBase4WithMemo = 0x8B, 78 | DBase4SQLTableWithMemo = 0xCB, 79 | FoxBaseWithMemo = 0xF5, 80 | HiPerSix = 0xE5, 81 | FoxBase2 = 0xFB, 82 | } 83 | 84 | impl FileType { 85 | pub fn from_u8(val: u8) -> Option { 86 | match val { 87 | 0x02 => Some(FileType::FoxBase), 88 | 0x03 => Some(FileType::DBase3Plus), 89 | 0x30 => Some(FileType::VisualFoxPro), 90 | 0x31 => Some(FileType::VisualFoxProAutoIncrement), 91 | 0x32 => Some(FileType::VisualFoxProVar), 92 | 0x43 => Some(FileType::DBase4SQLTable), 93 | 0x63 => Some(FileType::DBase4SQLSystem), 94 | 0x83 => Some(FileType::DBase3PlusWithMemo), 95 | 0x8B => Some(FileType::DBase4WithMemo), 96 | 0xCB => Some(FileType::DBase4SQLTableWithMemo), 97 | 0xF5 => Some(FileType::FoxBaseWithMemo), 98 | 0xE5 => Some(FileType::HiPerSix), 99 | 0xFB => Some(FileType::FoxBase2), 100 | _ => None, 101 | } 102 | } 103 | } 104 | 105 | #[derive(Debug, Clone)] 106 | pub struct Field { 107 | pub name: String, 108 | pub typ: char, 109 | pub len: u8, 110 | pub decimals: u8, 111 | pub work_area_id: u16, 112 | pub example: u8, 113 | pub mdx_flag: u8, 114 | } 115 | 116 | #[derive(Debug, Clone)] 117 | pub struct Record { 118 | pub deleted: bool, 119 | pub data: Vec, 120 | } 121 | 122 | #[derive(Debug)] 123 | pub struct File { 124 | pub header: Header, 125 | pub fields: Vec, 126 | reader: BufReader, 127 | } 128 | 129 | impl File { 130 | pub fn open(file: &str) -> anyhow::Result { 131 | let file = std::fs::File::open(file)?; 132 | let mut reader = BufReader::new(file); 133 | let header = Header::read(&mut reader)?; 134 | 135 | let mut fields = Vec::new(); 136 | loop { 137 | let field = Field::read(&mut reader)?; 138 | fields.push(field); 139 | 140 | let buf = reader.fill_buf()?; 141 | if buf.is_empty() { 142 | // End of file reached 143 | break; 144 | } 145 | 146 | if buf[0] == 0x0D { 147 | // Consume the 0x0D byte 148 | reader.consume(1); 149 | break; 150 | } 151 | } 152 | 153 | let buf = reader.fill_buf()?; 154 | if buf[0] == 0x00 { 155 | // Skip the terminator byte 156 | reader.consume(1); 157 | } 158 | 159 | let pos = reader.stream_position().unwrap(); 160 | let mut reader: BufReader = BufReader::new(reader.into_inner()); 161 | reader.seek(std::io::SeekFrom::Start(pos))?; 162 | 163 | Ok(Self { 164 | header, 165 | fields, 166 | reader, 167 | }) 168 | } 169 | 170 | pub fn num_records(&self) -> u64 { 171 | self.header.num_records as u64 172 | } 173 | } 174 | 175 | impl Field { 176 | pub fn read(reader: &mut R) -> anyhow::Result { 177 | let mut buffer = [0; 11]; 178 | reader.read_exact(&mut buffer)?; 179 | let zero_pos = buffer.iter().position(|&x| x == 0).unwrap(); 180 | let name = String::from_utf8(buffer[..zero_pos].to_vec())?; 181 | let typ = char::from_u32(reader.read_u8()? as u32).unwrap(); 182 | reader.seek(std::io::SeekFrom::Current(4))?; 183 | let len = reader.read_u8()?; 184 | let decimals = reader.read_u8()?; 185 | let mut work_area_id = [0; 2]; 186 | reader.read_exact(&mut work_area_id)?; 187 | let example = reader.read_u8()?; 188 | reader.seek(std::io::SeekFrom::Current(10))?; 189 | let mdx_flag = reader.read_u8()?; 190 | 191 | Ok(Self { 192 | name, 193 | typ, 194 | len, 195 | decimals, 196 | work_area_id: u16::from_le_bytes(work_area_id), 197 | example, 198 | mdx_flag, 199 | }) 200 | } 201 | } 202 | 203 | #[derive(Debug, Clone)] 204 | pub enum DbfType { 205 | Character(String), 206 | Numeric(String), 207 | Float(String), 208 | Logical(String), 209 | Date(String), 210 | Memo(String), 211 | } 212 | 213 | impl Record { 214 | pub fn read(dbf: &mut File) -> anyhow::Result { 215 | let mut buffer = [0; 1]; 216 | dbf.reader.read_exact(&mut buffer)?; 217 | let deleted = buffer[0] == 0x2A; 218 | 219 | let mut data = Vec::with_capacity(dbf.fields.len()); 220 | for field in &dbf.fields { 221 | let mut buffer = vec![0; field.len as usize]; 222 | dbf.reader.read_exact(&mut buffer)?; 223 | 224 | let value = match field.typ { 225 | 'C' => DbfType::Character(String::from_utf8_lossy(&buffer).to_string()), 226 | 'D' => DbfType::Date(String::from_utf8(buffer)?), 227 | 'F' => DbfType::Float(String::from_utf8(buffer)?), 228 | 'L' => DbfType::Logical(String::from_utf8(buffer)?), 229 | 'M' => DbfType::Memo(String::from_utf8(buffer)?), 230 | 'N' => DbfType::Numeric(String::from_utf8(buffer)?), 231 | _ => anyhow::bail!("Unsupported field type: {}", field.typ), 232 | }; 233 | 234 | data.push(value); 235 | } 236 | 237 | Ok(Self { deleted, data }) 238 | } 239 | } 240 | 241 | impl Iterator for File { 242 | type Item = anyhow::Result; 243 | 244 | fn next(&mut self) -> Option { 245 | match Record::read(self) { 246 | Ok(rec) => Some(Ok(rec)), 247 | Err(e) => { 248 | if e.downcast_ref::() 249 | .map_or(false, |e| e.kind() == std::io::ErrorKind::UnexpectedEof) 250 | { 251 | None 252 | } else { 253 | Some(Err(e)) 254 | } 255 | } 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /tests/fixtures/dbase_03.dbf: -------------------------------------------------------------------------------- 1 |  NPoint_IDC TypeCShapeCCircular_DCNon_circulC<Flow_preseCConditionCCommentsC<Date_VisitDTimeC 2 | Max_PDOPNMax_HDOPNCorr_TypeC$Rcvr_TypeC$GPS_DateDGPS_TimeC 3 | Update_StaC$Feat_NameCDatafileCUnfilt_PosN 4 | Filt_PosN 5 | Data_DictiCGPS_WeekNGPS_SecondN GPS_HeightNVert_PrecNHorz_PrecNStd_DevNNorthingNEastingNPoint_IDN 0507121 CMP circular 12 no Good 2005071210:56:30am 5.2 2.0Postprocessed Code GeoXT 2005071210:56:52amNew Driveway 050712TR2819.cor 2 2MS4 1331 226625.000 1131.323 3.1 1.3 0.897088 557904.898 2212577.192 401 0507122 CMP circular 12 no Good 2005071210:57:34am 4.9 2.0Postprocessed Code GeoXT 2005071210:57:37amNew Driveway 050712TR2819.cor 1 1MS4 1331 226670.000 1125.142 2.8 1.3 557997.831 2212576.868 402 0507123 CMP circular 12 no Good 2005071210:59:03am 5.4 4.4Postprocessed Code GeoXT 2005071210:59:12amNew Driveway 050712TR2819.cor 1 1MS4 1331 226765.000 1127.570 2.2 3.5 558184.757 2212571.349 403 0507125 CMP circular 12 no Good 2005071211:02:43am 3.4 1.5Postprocessed Code GeoXT 2005071211:03:12amNew Driveway 050712TR2819.cor 1 1MS4 1331 227005.000 1125.364 3.2 1.6 558703.723 2212562.547 405 05071210 CMP circular 15 no Good 2005071211:15:20am 3.7 2.2Postprocessed Code GeoXT 2005071211:14:52amNew Driveway 050712TR2819.cor 1 1MS4 1331 227705.000 1118.605 1.8 2.1 558945.763 2212739.979 410 05071216 CMP circular 12 no Good 2005071212:13:23pm 4.4 1.8Postprocessed Code GeoXT 2005071212:13:57pmNew Driveway 050712TR2819.cor 1 1MS4 1331 231250.000 1117.390 3.1 1.2 559024.234 2212856.927 416 05071217 CMP circular 12 no Good 2005071212:16:46pm 4.4 1.8Postprocessed Code GeoXT 2005071212:17:12pmNew Driveway 050712TR2819.cor 1 1MS4 1331 231445.000 1125.714 3.2 1.3 559342.534 2213340.161 417 05071219 CMP circular 12 no Plugged 2005071212:22:55pm 4.4 1.8Postprocessed Code GeoXT 2005071212:22:22pmNew Driveway 050712TR2819.cor 1 1MS4 1331 231755.000 1110.786 2.5 1.1 559578.776 2213560.247 419 05071224 CMP circular 12 no Good 2005071212:37:17pm 4.1 1.7Postprocessed Code GeoXT 2005071212:38:32pmNew Driveway 050712TR2819.cor 1 1MS4 1331 232725.000 1077.924 2.8 1.4 560582.575 2213759.022 424 05071225 CMP circular 12 no Good 2005071212:39:48pm 4.0 1.7Postprocessed Code GeoXT 2005071212:39:52pmNew Driveway 050712TR2819.cor 1 1MS4 1331 232805.000 1082.990 2.0 1.0 560678.501 2213716.657 425 05071229 CMP circular 12 no Good 2005071212:49:05pm 3.7 1.7Postprocessed Code GeoXT 2005071212:49:07pmNew Driveway 050712TR2819.cor 1 1MS4 1331 233360.000 1096.860 2.4 1.2 560126.094 2213720.301 429 05071231 CMP circular 12 no Plugged 2005071212:53:58pm 3.0 1.6Postprocessed Code GeoXT 2005071212:54:02pmNew Driveway 050712TR2819.cor 1 1MS4 1331 233655.000 1105.113 1.8 1.1 559952.331 2213689.001 431 05071232 CMP circular 12 no Plugged 2005071212:55:47pm 3.5 1.7Postprocessed Code GeoXT 2005071212:55:47pmNew Driveway 050712TR2819.cor 2 2MS4 1331 233760.000 1101.939 2.1 1.1 1.223112 559870.352 2213661.918 432 05071236 CMP circular 12 no Plugged 2005071201:08:40pm 3.3 1.6Postprocessed Code GeoXT 2005071201:08:42pmNew Driveway 050712TR2819.cor 1 1MS4 1331 234535.000 1125.517 1.8 1.2 559195.031 2213046.199 436 --------------------------------------------------------------------------------