Major changes since last commit: - FiveSql2 SQL:1999 engine (10,458 LOC) — 43/43 ALL PASS - 21 compiler/runtime bugs fixed (short-circuit AND/OR, FOR LOOP, etc.) - @byref pass-by-reference via RefCell pattern - Mutable closure capture (EnsureLocalRef + RefCell sharing) - RTL: 400 → 479 functions (+79: file, string, datetime, hash, UTF-8) - DateTime/Timestamp fully working (hb_DateTime, hb_Hour/Min/Sec, display) - Reserved word guard (39 keywords blocked from function calls) - AEval arg order fix (element before index) - Closure capture redecl fix (unique _cap_ names per block) - Hash/string indexing in ArrayPush/ArrayPop - Harbour compat test suite: 51/51 - 4 docs: Porting Report, Implementation Plan, Optimization Plan, Commercialization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
182 lines
5.0 KiB
Go
182 lines
5.0 KiB
Go
// 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
|
|
}
|