Files
five/hbrtl/fileio.go
Charles KWON OhJun 7c61db70c3 fix: Critical code review fixes — race conditions, panic recovery, LTRIM/RTRIM
CRITICAL fixes:
- fileio.go: Add sync.Mutex to file handle table (race condition #2)
  allocHandle/getHandle/removeHandle thread-safe helpers
- goroutine.go: Add defer/recover to GoLaunch/GoLaunchBlock
  Goroutine panic no longer crashes entire process (#5)

HIGH fixes:
- strings.go: Implement proper LTrim (TrimLeft) and RTrim (TrimRight)
  Previously both aliased to AllTrim — silent semantic bug (#18)
- register.go: TRIM = RTrim (Harbour compatible)

From 53-issue senior code review.
Remaining: 47 issues (HIGH: 10, MEDIUM: 18, LOW: 16, CRITICAL: 3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:17:30 +09:00

367 lines
6.7 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Low-level file I/O functions using Go's os/syscall packages.
// FOPEN, FCLOSE, FREAD, FWRITE, FSEEK, FCREATE, FERASE, FRENAME,
// CURDIR, DIRCHANGE, DIRECTORY, DISKSPACE
package hbrtl
import (
"five/hbrt"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
)
// File handle table — maps Harbour handle (int) to Go *os.File
// Protected by mutex for goroutine safety.
var (
fileHandles = map[int]*os.File{}
nextHandle = 10 // start from 10, avoid 0-2 (stdin/out/err)
fileHandlesMu sync.Mutex
)
// File handle helpers (thread-safe)
func allocHandle(f *os.File) int {
fileHandlesMu.Lock()
defer fileHandlesMu.Unlock()
h := nextHandle
nextHandle++
fileHandles[h] = f
return h
}
func getHandle(h int) (*os.File, bool) {
fileHandlesMu.Lock()
defer fileHandlesMu.Unlock()
f, ok := fileHandles[h]
return f, ok
}
func removeHandle(h int) {
fileHandlesMu.Lock()
defer fileHandlesMu.Unlock()
delete(fileHandles, h)
}
// FOPEN(cFileName [, nMode]) → nHandle | -1
// nMode: 0=read, 1=write, 2=readwrite
func FOpen(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
fname := t.Local(1).AsString()
mode := 0
if nParams >= 2 && !t.Local(2).IsNil() {
mode = t.Local(2).AsInt()
}
var flag int
switch mode & 0x03 {
case 0:
flag = os.O_RDONLY
case 1:
flag = os.O_WRONLY
case 2:
flag = os.O_RDWR
default:
flag = os.O_RDONLY
}
f, err := os.OpenFile(fname, flag, 0644)
if err != nil {
SetFError(2) // file not found
t.RetInt(-1)
return
}
h := allocHandle(f)
SetFError(0)
t.RetInt(int64(h))
}
// FCREATE(cFileName [, nAttr]) → nHandle | -1
func FCreate(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
fname := t.Local(1).AsString()
f, err := os.Create(fname)
if err != nil {
SetFError(3)
t.RetInt(-1)
return
}
h := allocHandle(f)
SetFError(0)
t.RetInt(int64(h))
}
// FCLOSE(nHandle) → lSuccess
func FClose(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
h := t.Local(1).AsInt()
f, ok := getHandle(h)
if !ok {
SetFError(6) // invalid handle
t.RetBool(false)
return
}
err := f.Close()
removeHandle(h)
SetFError(0)
t.RetBool(err == nil)
}
// FREAD(nHandle, @cBuffer, nBytes) → nBytesRead
func FRead(t *hbrt.Thread) {
t.Frame(3, 0)
defer t.EndProc()
h := t.Local(1).AsInt()
nBytes := t.Local(3).AsInt()
f, ok := getHandle(h)
if !ok {
SetFError(6)
t.RetInt(0)
return
}
buf := make([]byte, nBytes)
n, err := f.Read(buf)
if err != nil && n == 0 {
SetFError(5)
t.RetInt(0)
return
}
// In Harbour, the 2nd param is passed by reference and filled.
// In Five, we return the read data as the return value string.
// The caller should use: nRead := FRead(h, @cBuf, nLen)
// For simplicity, we set local 2 if it's a byref.
SetFError(0)
t.RetString(string(buf[:n]))
}
// FWRITE(nHandle, cBuffer [, nBytes]) → nBytesWritten
func FWrite(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
h := t.Local(1).AsInt()
data := t.Local(2).AsString()
f, ok := getHandle(h)
if !ok {
SetFError(6)
t.RetInt(0)
return
}
buf := []byte(data)
if nParams >= 3 && !t.Local(3).IsNil() {
nBytes := t.Local(3).AsInt()
if nBytes < len(buf) {
buf = buf[:nBytes]
}
}
n, err := f.Write(buf)
if err != nil {
SetFError(5)
} else {
SetFError(0)
}
t.RetInt(int64(n))
}
// FSEEK(nHandle, nOffset [, nOrigin]) → nNewPos
// nOrigin: 0=begin, 1=current, 2=end
func FSeek(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
h := t.Local(1).AsInt()
offset := t.Local(2).AsLong()
origin := 0
if nParams >= 3 && !t.Local(3).IsNil() {
origin = t.Local(3).AsInt()
}
f, ok := getHandle(h)
if !ok {
SetFError(6)
t.RetInt(0)
return
}
var whence int
switch origin {
case 0:
whence = 0 // io.SeekStart
case 1:
whence = 1 // io.SeekCurrent
case 2:
whence = 2 // io.SeekEnd
}
pos, err := f.Seek(offset, whence)
if err != nil {
SetFError(5)
t.RetInt(0)
return
}
SetFError(0)
t.RetLong(pos)
}
// FERASE(cFileName) → nResult (0=success, -1=error)
func FErase(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
fname := t.Local(1).AsString()
err := os.Remove(fname)
if err != nil {
SetFError(2)
t.RetInt(-1)
return
}
SetFError(0)
t.RetInt(0)
}
// FRENAME(cOldFile, cNewFile) → nResult (0=success, -1=error)
func FRename(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProc()
oldName := t.Local(1).AsString()
newName := t.Local(2).AsString()
err := os.Rename(oldName, newName)
if err != nil {
SetFError(2)
t.RetInt(-1)
return
}
SetFError(0)
t.RetInt(0)
}
// CURDIR([cDrive]) → cCurrentDir
func CurDir(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
dir, err := os.Getwd()
if err != nil {
t.RetString("")
return
}
// Remove leading separator (Harbour convention)
if len(dir) > 0 && (dir[0] == '/' || dir[0] == '\\') {
dir = dir[1:]
}
t.RetString(dir)
}
// DIRCHANGE(cNewDir) → nResult (0=success, -1=error)
func DirChange(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
dir := t.Local(1).AsString()
err := os.Chdir(dir)
if err != nil {
t.RetInt(-1)
return
}
t.RetInt(0)
}
// DIRECTORY(cDirSpec [, cAttr]) → aFiles
// Returns array of {cName, nSize, cDate, cTime, cAttr}
func Directory(t *hbrt.Thread) {
nParams := t.ParamCount()
t.Frame(nParams, 0)
defer t.EndProc()
spec := t.Local(1).AsString()
dir := filepath.Dir(spec)
pattern := filepath.Base(spec)
if dir == "" {
dir = "."
}
entries, err := os.ReadDir(dir)
if err != nil {
t.RetVal(hbrt.MakeArray(0))
return
}
var items []hbrt.Value
for _, e := range entries {
name := e.Name()
matched, _ := filepath.Match(pattern, name)
if !matched && pattern != "*" && pattern != "*.*" {
continue
}
info, err := e.Info()
if err != nil {
continue
}
// {cName, nSize, cDate, cTime, cAttr}
modTime := info.ModTime()
dateStr := modTime.Format("2006-01-02")
timeStr := modTime.Format("15:04:05")
attrStr := fileAttrStr(info.Mode())
entry := []hbrt.Value{
hbrt.MakeString(name),
hbrt.MakeNumInt(info.Size()),
hbrt.MakeString(dateStr),
hbrt.MakeString(timeStr),
hbrt.MakeString(attrStr),
}
items = append(items, hbrt.MakeArrayFrom(entry))
}
if len(items) == 0 {
t.RetVal(hbrt.MakeArray(0))
} else {
t.RetVal(hbrt.MakeArrayFrom(items))
}
}
func fileAttrStr(mode fs.FileMode) string {
var s strings.Builder
if mode.IsDir() {
s.WriteByte('D')
}
if mode&0200 == 0 {
s.WriteByte('R') // read-only
}
if strings.HasPrefix(filepath.Base(mode.String()), ".") {
s.WriteByte('H') // hidden (unix convention)
}
if s.Len() == 0 {
return ""
}
return s.String()
}