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