Files
five/hbrdd/dbf/memo.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline
- Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch
- RTL: 351 Harbour-compatible functions
- RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization
- Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec)
- HB_FUNC API: Full Harbour C API compatible Go bridge
- Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT
- Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST
- Macro Compiler: Runtime AST parsing and evaluation
- Debugger: TUI debugger with source display, breakpoints, stepping
- FRB: Native + Pcode dual mode runtime binary
- Tests: 13 packages ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:41:50 +09:00

201 lines
5.0 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// FPT memo file handler for Five.
// Byte-compatible with Harbour/FoxPro FPT memo files.
//
// FPT format:
// Header (512 bytes): nextBlock(4 BE), reserved(2), blockSize(2 BE), reserved(504)
// Blocks: type(4 BE) + size(4 BE) + data(variable)
//
// DBFCDX uses FPT memo (not DBT). DBFNTX traditionally uses DBT but
// Five supports FPT for both via this handler.
//
// Reference:
// /mnt/d/harbour-core/src/rdd/dbffpt/dbffpt1.c
// docs/dbf-engine-spec.md Section 6
package dbf
import (
"encoding/binary"
"fmt"
"os"
)
// FPT constants
const (
FPTHeaderSize = 512
FPTDefaultBlock = 64 // FoxPro default
FPTBlockTypeMemo = 1 // memo text
)
// FPTHeader is the FPT memo file header (512 bytes on disk).
// All multi-byte fields are BIG-ENDIAN (unlike DBF which is LE).
type FPTHeader struct {
NextBlock uint32 // offset 0: next free block (BE)
Reserved1 uint16 // offset 4
BlockSize uint16 // offset 6: block size in bytes (BE)
// offset 8-511: reserved
}
// FPTFile handles FPT memo file operations.
type FPTFile struct {
file *os.File
header FPTHeader
blockSize int
}
// OpenFPT opens an existing FPT memo file.
func OpenFPT(path string) (*FPTFile, error) {
f, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("open FPT %s: %w", path, err)
}
// Read header (first 8 bytes, big-endian)
buf := make([]byte, 8)
if _, err := f.ReadAt(buf, 0); err != nil {
f.Close()
return nil, fmt.Errorf("read FPT header: %w", err)
}
hdr := FPTHeader{
NextBlock: binary.BigEndian.Uint32(buf[0:4]),
Reserved1: binary.BigEndian.Uint16(buf[4:6]),
BlockSize: binary.BigEndian.Uint16(buf[6:8]),
}
blockSize := int(hdr.BlockSize)
if blockSize == 0 {
blockSize = FPTDefaultBlock
}
return &FPTFile{
file: f,
header: hdr,
blockSize: blockSize,
}, nil
}
// CreateFPT creates a new FPT memo file.
func CreateFPT(path string, blockSize int) (*FPTFile, error) {
if blockSize <= 0 {
blockSize = FPTDefaultBlock
}
f, err := os.Create(path)
if err != nil {
return nil, fmt.Errorf("create FPT %s: %w", path, err)
}
// Header occupies enough blocks to fill FPTHeaderSize
headerBlocks := uint32(FPTHeaderSize / blockSize)
if FPTHeaderSize%blockSize != 0 {
headerBlocks++
}
hdr := FPTHeader{
NextBlock: headerBlocks,
BlockSize: uint16(blockSize),
}
// Write header
buf := make([]byte, FPTHeaderSize)
binary.BigEndian.PutUint32(buf[0:4], hdr.NextBlock)
binary.BigEndian.PutUint16(buf[4:6], hdr.Reserved1)
binary.BigEndian.PutUint16(buf[6:8], hdr.BlockSize)
if _, err := f.Write(buf); err != nil {
f.Close()
return nil, err
}
return &FPTFile{
file: f,
header: hdr,
blockSize: blockSize,
}, nil
}
// Close closes the FPT file.
func (fpt *FPTFile) Close() error {
return fpt.file.Close()
}
// ReadMemo reads memo data from a block number.
// Returns nil for block 0 (empty memo).
func (fpt *FPTFile) ReadMemo(blockNo uint32) ([]byte, error) {
if blockNo == 0 {
return nil, nil
}
offset := int64(blockNo) * int64(fpt.blockSize)
// Read block header: type(4 BE) + size(4 BE)
hdr := make([]byte, 8)
if _, err := fpt.file.ReadAt(hdr, offset); err != nil {
return nil, fmt.Errorf("read memo block %d header: %w", blockNo, err)
}
// blockType := binary.BigEndian.Uint32(hdr[0:4]) // 0=picture, 1=memo, 2=object
dataSize := binary.BigEndian.Uint32(hdr[4:8])
if dataSize == 0 {
return nil, nil
}
// Read data
data := make([]byte, dataSize)
if _, err := fpt.file.ReadAt(data, offset+8); err != nil {
return nil, fmt.Errorf("read memo block %d data: %w", blockNo, err)
}
return data, nil
}
// WriteMemo writes memo data and returns the block number.
// Appends at the next available block.
func (fpt *FPTFile) WriteMemo(data []byte) (uint32, error) {
if len(data) == 0 {
return 0, nil // empty memo = block 0
}
blockNo := fpt.header.NextBlock
offset := int64(blockNo) * int64(fpt.blockSize)
// Write block header: type(4 BE) + size(4 BE)
hdr := make([]byte, 8)
binary.BigEndian.PutUint32(hdr[0:4], FPTBlockTypeMemo)
binary.BigEndian.PutUint32(hdr[4:8], uint32(len(data)))
if _, err := fpt.file.WriteAt(hdr, offset); err != nil {
return 0, fmt.Errorf("write memo block header: %w", err)
}
// Write data
if _, err := fpt.file.WriteAt(data, offset+8); err != nil {
return 0, fmt.Errorf("write memo block data: %w", err)
}
// Calculate blocks used
totalBytes := 8 + len(data) // header + data
blocksUsed := totalBytes / fpt.blockSize
if totalBytes%fpt.blockSize != 0 {
blocksUsed++
}
// Update next block pointer
fpt.header.NextBlock = blockNo + uint32(blocksUsed)
fpt.updateHeader()
return blockNo, nil
}
// updateHeader writes the header back to file.
func (fpt *FPTFile) updateHeader() {
buf := make([]byte, 8)
binary.BigEndian.PutUint32(buf[0:4], fpt.header.NextBlock)
binary.BigEndian.PutUint16(buf[4:6], fpt.header.Reserved1)
binary.BigEndian.PutUint16(buf[6:8], fpt.header.BlockSize)
fpt.file.WriteAt(buf, 0)
}