// 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) }