ollie/lib9p/lib9p.go

514 lines
9.9 KiB
Go

// Package lib9p provides a C-compatible shared library for 9P client operations.
// Build with: go build -buildmode=c-shared -o libollie9p.so
//
// This is the single 9P client implementation used by:
// - ollie-9p CLI tool
// - KDE GUI (via C bindings)
// - Any other C/C++ integration
package main
/*
#include <stdlib.h>
#include <string.h>
*/
import "C"
import (
"io"
"os"
"strings"
"sync"
"unsafe"
"9fans.net/go/plan9"
"9fans.net/go/plan9/client"
)
// Connection handle - opaque to C callers
type connHandle struct {
conn *client.Conn
fsys *client.Fsys
}
var (
handleMu sync.Mutex
handles = make(map[C.int]*connHandle)
nextHandle C.int = 1
)
func allocHandle(h *connHandle) C.int {
handleMu.Lock()
defer handleMu.Unlock()
id := nextHandle
nextHandle++
handles[id] = h
return id
}
func getHandle(id C.int) *connHandle {
handleMu.Lock()
defer handleMu.Unlock()
return handles[id]
}
func freeHandle(id C.int) {
handleMu.Lock()
defer handleMu.Unlock()
delete(handles, id)
}
// Error handling - last error per thread (simplified: global for now)
var lastError string
var lastErrMu sync.Mutex
func setError(err error) {
lastErrMu.Lock()
if err != nil {
lastError = err.Error()
} else {
lastError = ""
}
lastErrMu.Unlock()
}
//export ollie9p_last_error
func ollie9p_last_error() *C.char {
lastErrMu.Lock()
defer lastErrMu.Unlock()
if lastError == "" {
return nil
}
return C.CString(lastError)
}
//export ollie9p_free
func ollie9p_free(ptr unsafe.Pointer) {
C.free(ptr)
}
// ollie9p_connect establishes a connection to the 9P server.
// addr: socket path (e.g., "/tmp/ns.user.:0/ollie") or "tcp!host:port"
// user: username for Tauth/Tattach
// aname: attach name (identity for permissions)
// Returns: handle > 0 on success, -1 on error (call ollie9p_last_error)
//
//export ollie9p_connect
func ollie9p_connect(addr *C.char, user *C.char, aname *C.char) C.int {
goAddr := C.GoString(addr)
goUser := C.GoString(user)
goAname := C.GoString(aname)
network := "unix"
dialAddr := goAddr
if strings.HasPrefix(goAddr, "tcp!") {
network = "tcp"
dialAddr = strings.TrimPrefix(goAddr, "tcp!")
} else if strings.HasPrefix(goAddr, "unix!") {
dialAddr = strings.TrimPrefix(goAddr, "unix!")
} else if strings.Contains(goAddr, ":") && !strings.HasPrefix(goAddr, "/") {
network = "tcp"
}
conn, err := client.Dial(network, dialAddr)
if err != nil {
setError(err)
return -1
}
fsys, err := conn.Attach(nil, goUser, goAname)
if err != nil {
conn.Close()
setError(err)
return -1
}
h := &connHandle{conn: conn, fsys: fsys}
setError(nil)
return allocHandle(h)
}
// ollie9p_connect_default connects using NAMESPACE and USER env vars.
// Returns: handle > 0 on success, -1 on error
//
//export ollie9p_connect_default
func ollie9p_connect_default() C.int {
ns := os.Getenv("NAMESPACE")
if ns == "" {
ns = client.Namespace()
}
if ns == "" {
setError(&errString{"cannot determine namespace"})
return -1
}
addr := ns + "/ollie"
user := os.Getenv("USER")
if user == "" {
user = "none"
}
aname := os.Getenv("OLLIE_UNAME")
if aname == "" {
aname = user
}
cAddr := C.CString(addr)
cUser := C.CString(user)
cAname := C.CString(aname)
defer C.free(unsafe.Pointer(cAddr))
defer C.free(unsafe.Pointer(cUser))
defer C.free(unsafe.Pointer(cAname))
return ollie9p_connect(cAddr, cUser, cAname)
}
//export ollie9p_close
func ollie9p_close(handle C.int) {
h := getHandle(handle)
if h == nil {
return
}
if h.conn != nil {
h.conn.Close()
}
freeHandle(handle)
}
// ollie9p_read reads a file's contents.
// Returns: newly allocated buffer (caller must ollie9p_free), NULL on error.
// outLen receives the length of returned data.
//
//export ollie9p_read
func ollie9p_read(handle C.int, path *C.char, outLen *C.int) *C.char {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return nil
}
fid, err := h.fsys.Open(C.GoString(path), plan9.OREAD)
if err != nil {
setError(err)
return nil
}
defer fid.Close()
data, err := io.ReadAll(fid)
if err != nil {
setError(err)
return nil
}
setError(nil)
*outLen = C.int(len(data))
if len(data) == 0 {
return C.CString("")
}
// Allocate C memory and copy
cdata := C.CString(string(data))
return cdata
}
// ollie9p_read_bytes reads a file into a caller-provided buffer.
// Returns: bytes read, or -1 on error.
//
//export ollie9p_read_bytes
func ollie9p_read_bytes(handle C.int, path *C.char, buf *C.char, bufLen C.int) C.int {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return -1
}
fid, err := h.fsys.Open(C.GoString(path), plan9.OREAD)
if err != nil {
setError(err)
return -1
}
defer fid.Close()
// Read into Go slice backed by C memory
goBuf := unsafe.Slice((*byte)(unsafe.Pointer(buf)), int(bufLen))
n, err := io.ReadFull(fid, goBuf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
setError(err)
return -1
}
setError(nil)
return C.int(n)
}
// ollie9p_write writes data to a file.
// Returns: bytes written, or -1 on error.
//
//export ollie9p_write
func ollie9p_write(handle C.int, path *C.char, data *C.char, dataLen C.int) C.int {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return -1
}
fid, err := h.fsys.Open(C.GoString(path), plan9.OWRITE)
if err != nil {
setError(err)
return -1
}
goData := C.GoBytes(unsafe.Pointer(data), dataLen)
n, err := fid.Write(goData)
if err != nil {
fid.Close()
setError(err)
return -1
}
if err := fid.Close(); err != nil {
setError(err)
return -1
}
setError(nil)
return C.int(n)
}
// ollie9p_rdwr performs write-then-read on a request/response file.
// Returns: newly allocated response buffer, NULL on error.
//
//export ollie9p_rdwr
func ollie9p_rdwr(handle C.int, path *C.char, data *C.char, dataLen C.int, outLen *C.int) *C.char {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return nil
}
fid, err := h.fsys.Open(C.GoString(path), plan9.ORDWR)
if err != nil {
setError(err)
return nil
}
// Write
goData := C.GoBytes(unsafe.Pointer(data), dataLen)
if _, err := fid.Write(goData); err != nil {
fid.Close()
setError(err)
return nil
}
// Seek to beginning
if _, err := fid.Seek(0, 0); err != nil {
fid.Close()
setError(err)
return nil
}
// Read response
resp, err := io.ReadAll(fid)
fid.Close()
if err != nil {
setError(err)
return nil
}
setError(nil)
*outLen = C.int(len(resp))
return C.CString(string(resp))
}
// ollie9p_stat returns file metadata as "mode\tsize\tmtime\tname\n"
// Returns: newly allocated string, NULL on error.
//
//export ollie9p_stat
func ollie9p_stat(handle C.int, path *C.char) *C.char {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return nil
}
dir, err := h.fsys.Stat(C.GoString(path))
if err != nil {
setError(err)
return nil
}
setError(nil)
// Return tab-separated: mode, size, mtime, name
result := strings.Join([]string{
modeString(dir.Mode),
itoa(int64(dir.Length)),
itoa(int64(dir.Mtime)),
dir.Name,
}, "\t")
return C.CString(result)
}
// ollie9p_ls lists directory contents.
// Returns: newline-separated names, NULL on error.
//
//export ollie9p_ls
func ollie9p_ls(handle C.int, path *C.char) *C.char {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return nil
}
fid, err := h.fsys.Open(C.GoString(path), plan9.OREAD)
if err != nil {
setError(err)
return nil
}
defer fid.Close()
dirs, err := fid.Dirreadall()
if err != nil {
setError(err)
return nil
}
var names []string
for _, d := range dirs {
names = append(names, d.Name)
}
setError(nil)
return C.CString(strings.Join(names, "\n"))
}
// ollie9p_remove removes a file.
// Returns: 0 on success, -1 on error.
//
//export ollie9p_remove
func ollie9p_remove(handle C.int, path *C.char) C.int {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return -1
}
if err := h.fsys.Remove(C.GoString(path)); err != nil {
setError(err)
return -1
}
setError(nil)
return 0
}
// ollie9p_create creates a file.
// Returns: 0 on success, -1 on error.
//
//export ollie9p_create
func ollie9p_create(handle C.int, path *C.char, perm C.uint) C.int {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return -1
}
fid, err := h.fsys.Create(C.GoString(path), plan9.OWRITE, plan9.Perm(perm))
if err != nil {
setError(err)
return -1
}
fid.Close()
setError(nil)
return 0
}
// ollie9p_mkdir creates a directory.
// Returns: 0 on success, -1 on error.
//
//export ollie9p_mkdir
func ollie9p_mkdir(handle C.int, path *C.char) C.int {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return -1
}
fid, err := h.fsys.Create(C.GoString(path), plan9.OREAD, plan9.DMDIR|0755)
if err != nil {
setError(err)
return -1
}
fid.Close()
setError(nil)
return 0
}
// ollie9p_rename renames a file (Twstat).
// Returns: 0 on success, -1 on error.
//
//export ollie9p_rename
func ollie9p_rename(handle C.int, oldPath *C.char, newName *C.char) C.int {
h := getHandle(handle)
if h == nil {
setError(&errString{"invalid handle"})
return -1
}
if err := h.fsys.Wstat(C.GoString(oldPath), &plan9.Dir{Name: C.GoString(newName)}); err != nil {
setError(err)
return -1
}
setError(nil)
return 0
}
// Helper types
type errString struct{ s string }
func (e *errString) Error() string { return e.s }
func modeString(mode plan9.Perm) string {
var buf [10]byte
if mode&plan9.DMDIR != 0 {
buf[0] = 'd'
} else {
buf[0] = '-'
}
const rwx = "rwx"
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if mode&(1<<uint(8-i*3-j)) != 0 {
buf[1+i*3+j] = rwx[j]
} else {
buf[1+i*3+j] = '-'
}
}
}
return string(buf[:])
}
func itoa(n int64) string {
if n == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
neg := n < 0
if neg {
n = -n
}
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
buf[i] = '-'
}
return string(buf[i:])
}
func main() {}