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>
367 lines
6.7 KiB
Go
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()
|
|
}
|