Files
five/hbrdd/dbf/header.go
Charles KWON OhJun 486e466592 feat: FiveSql2 43/43, @byref, mutable closure, RTL 479, DateTime fix
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>
2026-04-11 11:35:37 +09:00

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
}