diff --git a/justfile b/justfile index be6bdfe..8add95d 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,8 @@ set dotenv-load := false home := env_var('HOME') cfg := home / ".config/ollie" bin := home / "bin" +lib := home / ".local/lib" +inc := home / ".local/include" # === Default (KF6) path === default: build install @@ -20,9 +22,9 @@ kf5: build-kf5 install-kf5 # === Build targets === -build: core ninep ollie-remote acme lsp-tools kde +build: core ninep lib9p ollie-remote acme lsp-tools kde -build-kf5: core ninep ollie-remote acme kde-kf5 +build-kf5: core ninep lib9p ollie-remote acme kde-kf5 core: @echo === Building ollie-core === @@ -33,6 +35,13 @@ ninep: go build -o {{bin}}/olliesrv ./cmd/olliesrv/ go build -o {{bin}}/ollie-9p ./cmd/ollie-9p/ +lib9p: + @echo === Building lib9p === + cd lib9p && go build -buildmode=c-shared -o libollie9p.so + mkdir -p {{lib}} {{inc}} + install -m755 lib9p/libollie9p.so {{lib}}/libollie9p.so + install -m644 lib9p/libollie9p.h {{inc}}/libollie9p.h + acme: @echo === Building ollie-acme === go build -o {{bin}}/Ollie ./cmd/Ollie/ diff --git a/lib9p/.gitignore b/lib9p/.gitignore new file mode 100644 index 0000000..63ae365 --- /dev/null +++ b/lib9p/.gitignore @@ -0,0 +1,5 @@ +# Build artifacts +*.so +*.o +*.a +test diff --git a/lib9p/go.mod b/lib9p/go.mod new file mode 100644 index 0000000..d2aebee --- /dev/null +++ b/lib9p/go.mod @@ -0,0 +1,5 @@ +module ollie/lib9p + +go 1.22 + +require 9fans.net/go v0.0.5 diff --git a/lib9p/go.sum b/lib9p/go.sum new file mode 100644 index 0000000..5089ea2 --- /dev/null +++ b/lib9p/go.sum @@ -0,0 +1,34 @@ +9fans.net/go v0.0.5 h1:u0H3Et5NowdVm56FDD+aWcDOao9zEo+zolNFsr8qyQk= +9fans.net/go v0.0.5/go.mod h1:Rxvbbc1e+1TyGMjAvLthGTyO97t+6JMQ6ly+Lcs9Uf0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20210405174845-4513512abef3/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lib9p/lib9p.go b/lib9p/lib9p.go new file mode 100644 index 0000000..db2fafc --- /dev/null +++ b/lib9p/lib9p.go @@ -0,0 +1,513 @@ +// 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 +#include +*/ +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< 0 { + i-- + buf[i] = byte('0' + n%10) + n /= 10 + } + if neg { + i-- + buf[i] = '-' + } + return string(buf[i:]) +} + +func main() {} diff --git a/lib9p/test.c b/lib9p/test.c new file mode 100644 index 0000000..43f51d5 --- /dev/null +++ b/lib9p/test.c @@ -0,0 +1,39 @@ +// Quick test of libollie9p +#include +#include +#include "libollie9p.h" + +int main() { + printf("Connecting to olliesrv...\n"); + int handle = ollie9p_connect_default(); + if (handle < 0) { + char *err = ollie9p_last_error(); + printf("Connect failed: %s\n", err ? err : "(unknown)"); + if (err) ollie9p_free(err); + return 1; + } + printf("Connected, handle=%d\n", handle); + + // Read session/idx + int len = 0; + char *data = ollie9p_read(handle, "session/idx", &len); + if (data) { + printf("session/idx (%d bytes):\n%s\n", len, data); + ollie9p_free(data); + } else { + char *err = ollie9p_last_error(); + printf("Read failed: %s\n", err ? err : "(unknown)"); + if (err) ollie9p_free(err); + } + + // List session directory + char *ls = ollie9p_ls(handle, "session"); + if (ls) { + printf("session/ listing:\n%s\n", ls); + ollie9p_free(ls); + } + + ollie9p_close(handle); + printf("Done.\n"); + return 0; +}