// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // DBF file header and field descriptor structures. // EXACT byte-compatible with Harbour's DBFHEADER and DBFFIELD. // // Reference: /mnt/d/harbour-core/include/hbdbf.h // docs/dbf-engine-spec.md package dbf import ( "encoding/binary" "fmt" "io" "time" ) // HeaderSize is the fixed size of the DBF file header. const HeaderSize = 32 // FieldDescSize is the fixed size of each field descriptor. const FieldDescSize = 32 // Header terminator byte. const HeaderTerminator = 0x0D // EOF marker byte. const EOFMarker = 0x1A // Deletion flag values. const ( RecordActive = ' ' // 0x20 RecordDeleted = '*' // 0x2A ) // Version byte values. const ( VersionDBF3 = 0x03 // Standard dBASE III VersionVFP = 0x30 // Visual FoxPro VersionVFPAuto = 0x31 // VFP + autoincrement VersionVFPVar = 0x32 // VFP + varchar/varbinary VersionDBT = 0x83 // dBASE III + DBT memo VersionFPT = 0xF5 // DBF + FPT memo ) // Header represents the 32-byte DBF file header. // Layout is byte-identical to Harbour's DBFHEADER. type Header struct { Version byte // offset 0 Year byte // offset 1 (year - 1900) Month byte // offset 2 Day byte // offset 3 RecCount uint32 // offset 4 (LE) HeaderLen uint16 // offset 8 (LE) RecordLen uint16 // offset 10 (LE) Reserved1 [2]byte Transaction byte // offset 14 Encrypted byte // offset 15 Reserved2 [12]byte HasTags byte // offset 28 CodePage byte // offset 29 Reserved3 [2]byte } // ReadHeader reads the 32-byte header from a reader. func ReadHeader(r io.Reader) (*Header, error) { h := &Header{} if err := binary.Read(r, binary.LittleEndian, h); err != nil { return nil, fmt.Errorf("read DBF header: %w", err) } return h, nil } // WriteHeader writes the 32-byte header to a writer. func WriteHeader(w io.Writer, h *Header) error { return binary.Write(w, binary.LittleEndian, h) } // UpdateDate sets the header's last-update date to now. func (h *Header) UpdateDate() { now := time.Now() h.Year = byte(now.Year() - 1900) h.Month = byte(now.Month()) h.Day = byte(now.Day()) } // FieldCount calculates the number of fields from header length. // Harbour: fieldCount = (headerLen - 32 - 1) / 32 func (h *Header) FieldCount() int { if h.HeaderLen < 33 { return 0 } return int(h.HeaderLen-32-1) / FieldDescSize } // HasMemo returns true if the version byte indicates a memo file. func (h *Header) HasMemo() bool { return h.Version == VersionDBT || h.Version == VersionFPT || h.Version == 0x8B || h.Version == 0xE6 || h.Version == 0xF6 } // RecordOffset calculates the file offset for a record (1-based). // Harbour: headerLen + (recNo - 1) * recordLen func (h *Header) RecordOffset(recNo uint32) int64 { return int64(h.HeaderLen) + int64(recNo-1)*int64(h.RecordLen) } // EOFOffset returns the file offset where EOF marker should be written. func (h *Header) EOFOffset() int64 { return int64(h.HeaderLen) + int64(h.RecCount)*int64(h.RecordLen) } // FieldDesc represents a 32-byte field descriptor. // Layout is byte-identical to Harbour's DBFFIELD. type FieldDesc struct { Name [11]byte // offset 0 (null-terminated) Type byte // offset 11 Reserved1 [4]byte // offset 12 Len byte // offset 16 Dec byte // offset 17 Flags byte // offset 18 Counter [4]byte // offset 19 (autoincrement, LE) Step byte // offset 23 Reserved2 [7]byte // offset 24 HasTag byte // offset 31 } // ReadFieldDescs reads n field descriptors from a reader. func ReadFieldDescs(r io.Reader, n int) ([]FieldDesc, error) { fields := make([]FieldDesc, n) for i := 0; i < n; i++ { if err := binary.Read(r, binary.LittleEndian, &fields[i]); err != nil { return nil, fmt.Errorf("read field descriptor %d: %w", i, err) } } return fields, nil } // WriteFieldDescs writes field descriptors to a writer. func WriteFieldDescs(w io.Writer, fields []FieldDesc) error { for i := range fields { if err := binary.Write(w, binary.LittleEndian, &fields[i]); err != nil { return fmt.Errorf("write field descriptor %d: %w", i, err) } } return nil } // GetName extracts the field name as a trimmed string. func (f *FieldDesc) GetName() string { n := len(f.Name) for i, b := range f.Name { if b == 0 { n = i break } } // Trim trailing spaces (DBF field names are space-padded) for n > 0 && f.Name[n-1] == ' ' { n-- } return string(f.Name[:n]) } // SetName sets the field name (max 10 chars, null-terminated, space-padded). func (f *FieldDesc) SetName(name string) { f.Name = [11]byte{} copy(f.Name[:], name) } // BuildFieldOffsets calculates the byte offset of each field within a record. // offset[0] = 1 (after deletion flag), offset[i+1] = offset[i] + field[i].Len // Harbour: pFieldOffset array built during OPEN. func BuildFieldOffsets(fields []FieldDesc) []uint16 { offsets := make([]uint16, len(fields)+1) offsets[0] = 1 // skip deletion flag byte for i, f := range fields { offsets[i+1] = offsets[i] + uint16(f.Len) } return offsets }