Compare commits
26 Commits
master
...
wip-bootst
| Author | SHA1 | Date | |
|---|---|---|---|
|
7ad8f15030
|
|||
|
993afde840
|
|||
|
c9cd16fd2a
|
|||
|
e42ea32dbe
|
|||
|
e7982b4ee9
|
|||
|
ef1ebf12d9
|
|||
|
775a9f57c9
|
|||
|
2f8ca83376
|
|||
|
3d720ada92
|
|||
|
2e5362e536
|
|||
|
6d3bd27220
|
|||
|
a27305cb4a
|
|||
|
0e476c5e5b
|
|||
|
54712e0426
|
|||
|
b77c1ecfdb
|
|||
|
dce5839a79
|
|||
|
d597592e1f
|
|||
|
056f5b12d4
|
|||
|
da2bb546ba
|
|||
|
7bfbd59810
|
|||
|
ea815a59e8
|
|||
|
28a8dc67d2
|
|||
|
ec49c63c5f
|
|||
|
5a50bf80ee
|
|||
|
ce06b7b663
|
|||
|
08bdc68f3a
|
@@ -72,6 +72,23 @@ jobs:
|
|||||||
path: result/*
|
path: result/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
sharefs:
|
||||||
|
name: ShareFS
|
||||||
|
runs-on: nix
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run NixOS test
|
||||||
|
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
|
||||||
|
|
||||||
|
- name: Upload test output
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "sharefs-vm-output"
|
||||||
|
path: result/*
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
hpkg:
|
hpkg:
|
||||||
name: Hpkg
|
name: Hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
@@ -96,6 +113,7 @@ jobs:
|
|||||||
- race
|
- race
|
||||||
- sandbox
|
- sandbox
|
||||||
- sandbox-race
|
- sandbox-race
|
||||||
|
- sharefs
|
||||||
- hpkg
|
- hpkg
|
||||||
runs-on: nix
|
runs-on: nix
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
282
cmd/sharefs/fuse-operations.c
Normal file
282
cmd/sharefs/fuse-operations.c
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#ifndef _GNU_SOURCE
|
||||||
|
#define _GNU_SOURCE /* O_DIRECT */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
|
||||||
|
#include "fuse-operations.h"
|
||||||
|
|
||||||
|
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
|
||||||
|
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
|
||||||
|
#define MUST_TRANSLATE_PATHNAME(pathname) \
|
||||||
|
do { \
|
||||||
|
if (pathname == NULL) \
|
||||||
|
return -EINVAL; \
|
||||||
|
while (*pathname == '/') \
|
||||||
|
pathname++; \
|
||||||
|
if (*pathname == '\0') \
|
||||||
|
pathname = "."; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
|
||||||
|
#define GET_CONTEXT_PRIV(ctx, priv) \
|
||||||
|
do { \
|
||||||
|
ctx = fuse_get_context(); \
|
||||||
|
priv = ctx->private_data; \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
|
||||||
|
* impl_getattr returns a negative errno style error code. */
|
||||||
|
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
|
||||||
|
/* allowlist of permitted types */
|
||||||
|
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
|
||||||
|
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
|
||||||
|
}
|
||||||
|
|
||||||
|
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
|
||||||
|
if (S_ISDIR(statbuf->st_mode))
|
||||||
|
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
|
||||||
|
else if (S_ISREG(statbuf->st_mode))
|
||||||
|
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
|
||||||
|
else
|
||||||
|
statbuf->st_mode = 0; /* should always be symlink in this case */
|
||||||
|
|
||||||
|
statbuf->st_uid = ctx->uid;
|
||||||
|
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
|
||||||
|
statbuf->st_ctim = statbuf->st_mtim;
|
||||||
|
statbuf->st_nlink = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fuse_operations implementation */
|
||||||
|
|
||||||
|
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
|
||||||
|
return -errno;
|
||||||
|
return impl_getattr(ctx, statbuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
|
||||||
|
int fd;
|
||||||
|
DIR *dp;
|
||||||
|
struct stat st;
|
||||||
|
int ret = 0;
|
||||||
|
struct dirent *de;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)offset;
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
|
||||||
|
return -errno;
|
||||||
|
if ((dp = fdopendir(fd)) == NULL) {
|
||||||
|
close(fd);
|
||||||
|
return -errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
errno = 0; /* for the next readdir call */
|
||||||
|
while ((de = readdir(dp)) != NULL) {
|
||||||
|
if (flags & FUSE_READDIR_PLUS) {
|
||||||
|
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
|
||||||
|
ret = -errno;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ret = impl_getattr(ctx, &st)) < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
|
||||||
|
} else
|
||||||
|
ret = filler(buf, de->d_name, NULL, 0, 0);
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
ret = errno != 0 ? -errno : -EIO; /* filler */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
errno = 0; /* for the next readdir call */
|
||||||
|
}
|
||||||
|
if (ret == 0 && errno != 0)
|
||||||
|
ret = -errno; /* readdir */
|
||||||
|
|
||||||
|
closedir(dp);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_mkdir(const char *pathname, mode_t mode) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)mode;
|
||||||
|
|
||||||
|
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_unlink(const char *pathname) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if (unlinkat(priv->dirfd, pathname, 0) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_rmdir(const char *pathname) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(oldpath);
|
||||||
|
MUST_TRANSLATE_PATHNAME(newpath);
|
||||||
|
|
||||||
|
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||||
|
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
|
||||||
|
int fd;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
|
||||||
|
return -errno;
|
||||||
|
if ((ret = ftruncate(fd, length)) == -1)
|
||||||
|
ret = -errno;
|
||||||
|
close(fd);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)fi;
|
||||||
|
|
||||||
|
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
(void)mode;
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
|
||||||
|
return -errno;
|
||||||
|
fi->fh = fd;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
|
||||||
|
return -errno;
|
||||||
|
fi->fh = fd;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
|
||||||
|
return -errno;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
|
||||||
|
return -errno;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
|
||||||
|
int fd;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
struct fuse_context *ctx;
|
||||||
|
struct sharefs_private *priv;
|
||||||
|
GET_CONTEXT_PRIV(ctx, priv);
|
||||||
|
MUST_TRANSLATE_PATHNAME(pathname);
|
||||||
|
|
||||||
|
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
|
||||||
|
return -errno;
|
||||||
|
if ((ret = fstatvfs(fd, statbuf)) == -1)
|
||||||
|
ret = -errno;
|
||||||
|
close(fd);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
return close(fi->fh);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
|
||||||
|
(void)pathname;
|
||||||
|
|
||||||
|
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
|
||||||
|
return -errno;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
34
cmd/sharefs/fuse-operations.h
Normal file
34
cmd/sharefs/fuse-operations.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
|
||||||
|
#include <fuse.h>
|
||||||
|
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
|
||||||
|
|
||||||
|
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
|
||||||
|
#error This package requires libfuse >= v3.12
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||||
|
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
||||||
|
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
||||||
|
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||||
|
|
||||||
|
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||||
|
struct sharefs_private {
|
||||||
|
int dirfd; /* source dirfd opened during sharefs_init */
|
||||||
|
uintptr_t setup; /* cgo handle of opaque setup state */
|
||||||
|
};
|
||||||
|
|
||||||
|
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
|
||||||
|
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
|
||||||
|
int sharefs_mkdir(const char *pathname, mode_t mode);
|
||||||
|
int sharefs_unlink(const char *pathname);
|
||||||
|
int sharefs_rmdir(const char *pathname);
|
||||||
|
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
|
||||||
|
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
|
||||||
|
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
|
||||||
|
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
|
||||||
|
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
|
||||||
|
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||||
|
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||||
|
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
|
||||||
|
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
|
||||||
|
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);
|
||||||
556
cmd/sharefs/fuse.go
Normal file
556
cmd/sharefs/fuse.go
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: --static fuse3
|
||||||
|
|
||||||
|
#include "fuse-operations.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
|
||||||
|
extern void sharefs_destroy(void *private_data);
|
||||||
|
|
||||||
|
typedef void (*closure)();
|
||||||
|
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/container/std"
|
||||||
|
"hakurei.app/hst"
|
||||||
|
"hakurei.app/internal/helper/proc"
|
||||||
|
"hakurei.app/internal/info"
|
||||||
|
"hakurei.app/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// closure represents a C function pointer.
|
||||||
|
closure = C.closure
|
||||||
|
|
||||||
|
// fuseArgs represents the fuse_args structure.
|
||||||
|
fuseArgs = C.struct_fuse_args
|
||||||
|
|
||||||
|
// setupState holds state used for setup. Its cgo handle is included in
|
||||||
|
// sharefs_private and considered opaque to non-setup callbacks.
|
||||||
|
setupState struct {
|
||||||
|
// Whether sharefs_init failed.
|
||||||
|
initFailed bool
|
||||||
|
|
||||||
|
// Whether to create source directory as root.
|
||||||
|
mkdir bool
|
||||||
|
|
||||||
|
// Open file descriptor to fuse.
|
||||||
|
Fuse int
|
||||||
|
|
||||||
|
// Pathname to open for dirfd.
|
||||||
|
Source *check.Absolute
|
||||||
|
// New uid and gid to set by sharefs_init when starting as root.
|
||||||
|
Setuid, Setgid int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { gob.Register(new(setupState)) }
|
||||||
|
|
||||||
|
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
|
||||||
|
func destroySetup(private_data unsafe.Pointer) (ok bool) {
|
||||||
|
if private_data == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
priv := (*C.struct_sharefs_private)(private_data)
|
||||||
|
|
||||||
|
if h := cgo.Handle(priv.setup); h != 0 {
|
||||||
|
priv.setup = 0
|
||||||
|
h.Delete()
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//export sharefs_init
|
||||||
|
func sharefs_init(_ *C.struct_fuse_conn_info, cfg *C.struct_fuse_config) unsafe.Pointer {
|
||||||
|
ctx := C.fuse_get_context()
|
||||||
|
priv := (*C.struct_sharefs_private)(ctx.private_data)
|
||||||
|
setup := cgo.Handle(priv.setup).Value().(*setupState)
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
log.Println("filesystem daemon must not run as root")
|
||||||
|
goto fail
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.use_ino = C.true
|
||||||
|
cfg.direct_io = C.false
|
||||||
|
// getattr is context-dependent
|
||||||
|
cfg.attr_timeout = 0
|
||||||
|
cfg.entry_timeout = 0
|
||||||
|
cfg.negative_timeout = 0
|
||||||
|
|
||||||
|
// all future filesystem operations happen through this dirfd
|
||||||
|
if fd, err := syscall.Open(setup.Source.String(), syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC, 0); err != nil {
|
||||||
|
log.Printf("cannot open %q: %v", setup.Source, err)
|
||||||
|
goto fail
|
||||||
|
} else if err = syscall.Fchdir(fd); err != nil {
|
||||||
|
_ = syscall.Close(fd)
|
||||||
|
log.Printf("cannot enter %q: %s", setup.Source, err)
|
||||||
|
goto fail
|
||||||
|
} else {
|
||||||
|
priv.dirfd = C.int(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.private_data
|
||||||
|
|
||||||
|
fail:
|
||||||
|
setup.initFailed = true
|
||||||
|
C.fuse_exit(ctx.fuse)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//export sharefs_destroy
|
||||||
|
func sharefs_destroy(private_data unsafe.Pointer) {
|
||||||
|
if private_data != nil {
|
||||||
|
destroySetup(private_data)
|
||||||
|
priv := (*C.struct_sharefs_private)(private_data)
|
||||||
|
|
||||||
|
if err := syscall.Close(int(priv.dirfd)); err != nil {
|
||||||
|
log.Printf("cannot close source directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showHelp prints the help message.
|
||||||
|
func showHelp(args *fuseArgs) {
|
||||||
|
executableName := sharefsName
|
||||||
|
if args.argc > 0 {
|
||||||
|
executableName = path.Base(C.GoString(*args.argv))
|
||||||
|
} else if name, err := os.Executable(); err == nil {
|
||||||
|
executableName = path.Base(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
||||||
|
|
||||||
|
fmt.Println("Filesystem options:")
|
||||||
|
fmt.Println(" -o source=/data/media source directory to be mounted")
|
||||||
|
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
|
||||||
|
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
|
||||||
|
|
||||||
|
fmt.Println("\nFUSE options:")
|
||||||
|
C.fuse_cmdline_help()
|
||||||
|
C.fuse_lib_help(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOpts parses fuse options via fuse_opt_parse.
|
||||||
|
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||||
|
var unsafeOpts struct {
|
||||||
|
// Pathname to writable source directory.
|
||||||
|
source *C.char
|
||||||
|
|
||||||
|
// Whether to create source directory as root.
|
||||||
|
mkdir C.int
|
||||||
|
|
||||||
|
// Decimal string representation of uid to set when running as root.
|
||||||
|
setuid *C.char
|
||||||
|
// Decimal string representation of gid to set when running as root.
|
||||||
|
setgid *C.char
|
||||||
|
|
||||||
|
// Decimal string representation of open file descriptor to read setupState from.
|
||||||
|
// This is an internal detail for containerisation and must not be specified directly.
|
||||||
|
setup *C.char
|
||||||
|
}
|
||||||
|
|
||||||
|
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
|
||||||
|
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
|
||||||
|
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
||||||
|
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
|
||||||
|
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
|
||||||
|
|
||||||
|
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
|
||||||
|
|
||||||
|
C._FUSE_OPT_END(),
|
||||||
|
}[0], nil) == -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafeOpts.source != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.source))
|
||||||
|
}
|
||||||
|
if unsafeOpts.setuid != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
|
||||||
|
}
|
||||||
|
if unsafeOpts.setgid != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafeOpts.setup != nil {
|
||||||
|
defer C.free(unsafe.Pointer(unsafeOpts.setup))
|
||||||
|
|
||||||
|
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
|
||||||
|
log.Println("invalid value for option setup")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
r := os.NewFile(uintptr(v), "setup")
|
||||||
|
defer func() {
|
||||||
|
if err = r.Close(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err = gob.NewDecoder(r).Decode(setup); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if setup.Fuse < 3 {
|
||||||
|
log.Println("invalid file descriptor", setup.Fuse)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafeOpts.source == nil {
|
||||||
|
showHelp(args)
|
||||||
|
return false
|
||||||
|
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
setup.Source = a
|
||||||
|
}
|
||||||
|
setup.mkdir = unsafeOpts.mkdir != 0
|
||||||
|
|
||||||
|
if unsafeOpts.setuid == nil {
|
||||||
|
setup.Setuid = -1
|
||||||
|
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
|
||||||
|
log.Println("invalid value for option setuid")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
setup.Setuid = v
|
||||||
|
}
|
||||||
|
if unsafeOpts.setgid == nil {
|
||||||
|
setup.Setgid = -1
|
||||||
|
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
|
||||||
|
log.Println("invalid value for option setgid")
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
setup.Setgid = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyArgs returns a heap allocated copy of an argument slice in fuse_args representation.
|
||||||
|
func copyArgs(s ...string) fuseArgs {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return fuseArgs{argc: 0, argv: nil, allocated: 0}
|
||||||
|
}
|
||||||
|
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
|
||||||
|
for i, arg := range s {
|
||||||
|
args[i] = C.CString(arg)
|
||||||
|
}
|
||||||
|
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// freeArgs frees the contents of argument list.
|
||||||
|
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
|
||||||
|
|
||||||
|
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
|
||||||
|
// The last byte of arg must be 0.
|
||||||
|
func unsafeAddArgument(args *fuseArgs, arg string) {
|
||||||
|
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func _main(s ...string) (exitCode int) {
|
||||||
|
msg := message.New(log.Default())
|
||||||
|
container.TryArgv0(msg)
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
// don't mask creation mode, kernel already did that
|
||||||
|
syscall.Umask(0)
|
||||||
|
|
||||||
|
var pinner runtime.Pinner
|
||||||
|
defer pinner.Unpin()
|
||||||
|
|
||||||
|
args := copyArgs(s...)
|
||||||
|
defer freeArgs(&args)
|
||||||
|
|
||||||
|
// this causes the kernel to enforce access control based on
|
||||||
|
// struct stat populated by sharefs_getattr
|
||||||
|
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||||
|
|
||||||
|
var priv C.struct_sharefs_private
|
||||||
|
pinner.Pin(&priv)
|
||||||
|
var setup setupState
|
||||||
|
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
|
||||||
|
defer destroySetup(unsafe.Pointer(&priv))
|
||||||
|
|
||||||
|
var opts C.struct_fuse_cmdline_opts
|
||||||
|
if C.fuse_parse_cmdline(&args, &opts) != 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if opts.mountpoint != nil {
|
||||||
|
defer C.free(unsafe.Pointer(opts.mountpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.show_version != 0 {
|
||||||
|
fmt.Println("hakurei version", info.Version())
|
||||||
|
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
|
||||||
|
C.fuse_lowlevel_version()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.show_help != 0 {
|
||||||
|
showHelp(&args)
|
||||||
|
return 0
|
||||||
|
} else if opts.mountpoint == nil {
|
||||||
|
log.Println("no mountpoint specified")
|
||||||
|
return 2
|
||||||
|
} else {
|
||||||
|
// hack to keep fuse_parse_cmdline happy in the container
|
||||||
|
mountpoint := C.GoString(opts.mountpoint)
|
||||||
|
pathnameArg := -1
|
||||||
|
for i, arg := range s {
|
||||||
|
if arg == mountpoint {
|
||||||
|
pathnameArg = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pathnameArg < 0 {
|
||||||
|
log.Println("mountpoint must be absolute")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
s[pathnameArg] = container.Nonexistent
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
asRoot := os.Geteuid() == 0
|
||||||
|
|
||||||
|
if asRoot {
|
||||||
|
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||||
|
log.Println("setuid and setgid must not be 0")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.Fuse >= 3 {
|
||||||
|
log.Println("filesystem daemon must not run as root")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.mkdir {
|
||||||
|
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
log.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
// skip setup for existing source directory
|
||||||
|
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
||||||
|
log.Println("setuid and setgid has no effect when not starting as root")
|
||||||
|
return 1
|
||||||
|
} else if setup.mkdir {
|
||||||
|
log.Println("mkdir has no effect when not starting as root")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
op := C.struct_fuse_operations{
|
||||||
|
init: closure(C.sharefs_init),
|
||||||
|
destroy: closure(C.sharefs_destroy),
|
||||||
|
|
||||||
|
// implemented in fuse-helper.c
|
||||||
|
getattr: closure(C.sharefs_getattr),
|
||||||
|
readdir: closure(C.sharefs_readdir),
|
||||||
|
mkdir: closure(C.sharefs_mkdir),
|
||||||
|
unlink: closure(C.sharefs_unlink),
|
||||||
|
rmdir: closure(C.sharefs_rmdir),
|
||||||
|
rename: closure(C.sharefs_rename),
|
||||||
|
truncate: closure(C.sharefs_truncate),
|
||||||
|
utimens: closure(C.sharefs_utimens),
|
||||||
|
create: closure(C.sharefs_create),
|
||||||
|
open: closure(C.sharefs_open),
|
||||||
|
read: closure(C.sharefs_read),
|
||||||
|
write: closure(C.sharefs_write),
|
||||||
|
statfs: closure(C.sharefs_statfs),
|
||||||
|
release: closure(C.sharefs_release),
|
||||||
|
fsync: closure(C.sharefs_fsync),
|
||||||
|
}
|
||||||
|
|
||||||
|
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
|
||||||
|
if fuse == nil {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
defer C.fuse_destroy(fuse)
|
||||||
|
se := C.fuse_get_session(fuse)
|
||||||
|
|
||||||
|
if setup.Fuse < 3 {
|
||||||
|
// unconfined, set up mount point and container
|
||||||
|
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
// unmounted by initial process
|
||||||
|
defer func() {
|
||||||
|
if exitCode == 5 {
|
||||||
|
C.fuse_unmount(fuse)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if asRoot {
|
||||||
|
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
||||||
|
log.Printf("cannot set gid: %v", err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if err := syscall.Setgroups(nil); err != nil {
|
||||||
|
log.Printf("cannot set supplementary groups: %v", err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
|
||||||
|
log.Printf("cannot set uid: %v", err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.SwapVerbose(opts.debug != 0)
|
||||||
|
ctx := context.Background()
|
||||||
|
if opts.foreground != 0 {
|
||||||
|
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
ctx = c
|
||||||
|
}
|
||||||
|
z := container.New(ctx, msg)
|
||||||
|
z.AllowOrphan = opts.foreground == 0
|
||||||
|
z.Env = os.Environ()
|
||||||
|
|
||||||
|
// keep fuse_parse_cmdline happy in the container
|
||||||
|
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
|
||||||
|
|
||||||
|
if a, err := check.NewAbs(container.MustExecutable(msg)); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
} else {
|
||||||
|
z.Path = a
|
||||||
|
}
|
||||||
|
z.Args = s
|
||||||
|
z.ForwardCancel = true
|
||||||
|
z.SeccompPresets |= std.PresetStrict
|
||||||
|
z.ParentPerm = 0700
|
||||||
|
z.Bind(setup.Source, setup.Source, std.BindWritable)
|
||||||
|
if !z.AllowOrphan {
|
||||||
|
z.WaitDelay = hst.WaitDelayMax
|
||||||
|
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||||
|
}
|
||||||
|
z.Bind(z.Path, z.Path, 0)
|
||||||
|
setup.Fuse = int(proc.ExtraFileSlice(&z.ExtraFiles, os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse")))
|
||||||
|
|
||||||
|
var setupWriter io.WriteCloser
|
||||||
|
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
} else {
|
||||||
|
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||||
|
setupWriter = w
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := z.Start(); err != nil {
|
||||||
|
if m, ok := message.GetMessage(err); ok {
|
||||||
|
log.Println(m)
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if err := z.Serve(); err != nil {
|
||||||
|
if m, ok := message.GetMessage(err); ok {
|
||||||
|
log.Println(m)
|
||||||
|
} else {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
} else if err = setupWriter.Close(); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !z.AllowOrphan {
|
||||||
|
if err := z.Wait(); err != nil {
|
||||||
|
var exitError *exec.ExitError
|
||||||
|
if !errors.As(err, &exitError) || exitError == nil {
|
||||||
|
log.Println(err)
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
|
||||||
|
default:
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
} else { // confined
|
||||||
|
C.free(unsafe.Pointer(opts.mountpoint))
|
||||||
|
// must be heap allocated
|
||||||
|
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
|
||||||
|
|
||||||
|
if err := os.Chdir("/"); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
defer C.fuse_unmount(fuse)
|
||||||
|
|
||||||
|
if C.fuse_set_signal_handlers(se) != 0 {
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
defer C.fuse_remove_signal_handlers(se)
|
||||||
|
|
||||||
|
if opts.singlethread != 0 {
|
||||||
|
if C.fuse_loop(fuse) != 0 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loopConfig := C.fuse_loop_cfg_create()
|
||||||
|
if loopConfig == nil {
|
||||||
|
return 7
|
||||||
|
}
|
||||||
|
defer C.fuse_loop_cfg_destroy(loopConfig)
|
||||||
|
|
||||||
|
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
|
||||||
|
|
||||||
|
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
|
||||||
|
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
|
||||||
|
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if setup.initFailed {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
113
cmd/sharefs/fuse_test.go
Normal file
113
cmd/sharefs/fuse_test.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseOpts(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want setupState
|
||||||
|
wantLog string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"zero length", []string{}, setupState{}, "", false},
|
||||||
|
|
||||||
|
{"not absolute", []string{"sharefs",
|
||||||
|
"-o", "source=nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
|
||||||
|
|
||||||
|
{"not specified", []string{"sharefs",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{}, "", false},
|
||||||
|
|
||||||
|
{"invalid setuid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=ff",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
}, "sharefs: invalid value for option setuid\n", false},
|
||||||
|
|
||||||
|
{"invalid setgid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=ff",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: 1023,
|
||||||
|
}, "sharefs: invalid value for option setgid\n", false},
|
||||||
|
|
||||||
|
{"simple", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: -1,
|
||||||
|
Setgid: -1,
|
||||||
|
}, "", true},
|
||||||
|
|
||||||
|
{"root", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: 1023,
|
||||||
|
Setgid: 1023,
|
||||||
|
}, "", true},
|
||||||
|
|
||||||
|
{"setuid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setuid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: 1023,
|
||||||
|
Setgid: -1,
|
||||||
|
}, "", true},
|
||||||
|
|
||||||
|
{"setgid", []string{"sharefs",
|
||||||
|
"-o", "source=/proc/nonexistent",
|
||||||
|
"-o", "setgid=1023",
|
||||||
|
}, setupState{
|
||||||
|
Source: check.MustAbs("/proc/nonexistent"),
|
||||||
|
Setuid: -1,
|
||||||
|
Setgid: 1023,
|
||||||
|
}, "", true},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
got setupState
|
||||||
|
buf bytes.Buffer
|
||||||
|
)
|
||||||
|
args := copyArgs(tc.args...)
|
||||||
|
defer freeArgs(&args)
|
||||||
|
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||||
|
|
||||||
|
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
|
||||||
|
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(&got, &tc.want) {
|
||||||
|
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf.String() != tc.wantLog {
|
||||||
|
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
31
cmd/sharefs/main.go
Normal file
31
cmd/sharefs/main.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sharefsName is the prefix used by log.std in the sharefs process.
|
||||||
|
const sharefsName = "sharefs"
|
||||||
|
|
||||||
|
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
||||||
|
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
||||||
|
//
|
||||||
|
// In this case, args always has a length of 5 with index 0 being what comes
|
||||||
|
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
||||||
|
// to mount (sharefsName is used as the magic string to enable this hack),
|
||||||
|
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
||||||
|
func handleMountArgs(args []string) []string {
|
||||||
|
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
||||||
|
return []string{sharefsName, args[2], "-o", args[4]}
|
||||||
|
}
|
||||||
|
return slices.Clone(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix(sharefsName + ": ")
|
||||||
|
|
||||||
|
os.Exit(_main(handleMountArgs(os.Args)...))
|
||||||
|
}
|
||||||
29
cmd/sharefs/main_test.go
Normal file
29
cmd/sharefs/main_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleMountArgs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{"nil", nil, nil},
|
||||||
|
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
||||||
|
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
||||||
|
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
41
cmd/sharefs/test/configuration.nix
Normal file
41
cmd/sharefs/test/configuration.nix
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
users.users = {
|
||||||
|
alice = {
|
||||||
|
isNormalUser = true;
|
||||||
|
description = "Alice Foobar";
|
||||||
|
password = "foobar";
|
||||||
|
uid = 1000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
home-manager.users.alice.home.stateVersion = "24.11";
|
||||||
|
|
||||||
|
# Automatically login on tty1 as a normal user:
|
||||||
|
services.getty.autologinUser = "alice";
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# For benchmarking sharefs:
|
||||||
|
systemPackages = [ pkgs.fsmark ];
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualisation = {
|
||||||
|
diskSize = 6 * 1024;
|
||||||
|
|
||||||
|
qemu.options = [
|
||||||
|
# Increase test performance:
|
||||||
|
"-smp 8"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.hakurei = rec {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/var/lib/hakurei";
|
||||||
|
sharefs.source = "${stateDir}/sdcard";
|
||||||
|
users.alice = 0;
|
||||||
|
|
||||||
|
extraHomeConfig = {
|
||||||
|
home.stateVersion = "23.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
44
cmd/sharefs/test/default.nix
Normal file
44
cmd/sharefs/test/default.nix
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
testers,
|
||||||
|
|
||||||
|
system,
|
||||||
|
self,
|
||||||
|
}:
|
||||||
|
testers.nixosTest {
|
||||||
|
name = "sharefs";
|
||||||
|
nodes.machine =
|
||||||
|
{ options, pkgs, ... }:
|
||||||
|
let
|
||||||
|
fhs =
|
||||||
|
let
|
||||||
|
hakurei = options.environment.hakurei.package.default;
|
||||||
|
in
|
||||||
|
pkgs.buildFHSEnv {
|
||||||
|
pname = "hakurei-fhs";
|
||||||
|
inherit (hakurei) version;
|
||||||
|
targetPkgs = _: hakurei.targetPkgs;
|
||||||
|
extraOutputsToInstall = [ "dev" ];
|
||||||
|
profile = ''
|
||||||
|
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
environment.systemPackages = [
|
||||||
|
# For go tests:
|
||||||
|
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
||||||
|
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
||||||
|
${fhs}/bin/hakurei-fhs -c 'CC="clang -O3 -Werror" go test ./...'
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
./configuration.nix
|
||||||
|
|
||||||
|
self.nixosModules.hakurei
|
||||||
|
self.inputs.home-manager.nixosModules.home-manager
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = builtins.readFile ./test.py;
|
||||||
|
}
|
||||||
60
cmd/sharefs/test/test.py
Normal file
60
cmd/sharefs/test/test.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
start_all()
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# To check sharefs version:
|
||||||
|
print(machine.succeed("sharefs -V"))
|
||||||
|
|
||||||
|
# Make sure sharefs started:
|
||||||
|
machine.wait_for_unit("sdcard.mount")
|
||||||
|
|
||||||
|
machine.succeed("mkdir /mnt")
|
||||||
|
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
||||||
|
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||||
|
if output != want:
|
||||||
|
raise Exception(f"unexpected output: {output}")
|
||||||
|
|
||||||
|
# Malformed setuid/setgid representation:
|
||||||
|
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
|
||||||
|
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
|
||||||
|
|
||||||
|
# Bounds check for setuid/setgid:
|
||||||
|
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
|
||||||
|
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
|
||||||
|
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
|
||||||
|
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
||||||
|
|
||||||
|
# Non-root setuid/setgid:
|
||||||
|
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||||
|
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||||
|
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||||
|
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
||||||
|
|
||||||
|
# Starting as root without setuid/setgid:
|
||||||
|
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||||
|
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||||
|
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||||
|
|
||||||
|
# Make sure nothing actually got mounted:
|
||||||
|
machine.fail("umount /mnt")
|
||||||
|
machine.succeed("rmdir /mnt")
|
||||||
|
|
||||||
|
# Unprivileged mount/unmount:
|
||||||
|
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
||||||
|
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
||||||
|
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
||||||
|
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
||||||
|
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
||||||
|
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
||||||
|
|
||||||
|
# Benchmark sharefs:
|
||||||
|
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
|
||||||
|
machine.copy_from_vm("/tmp/fs_log.txt", "")
|
||||||
|
|
||||||
|
# Check permissions:
|
||||||
|
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||||
|
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||||
|
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
|
||||||
|
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
|
||||||
|
|
||||||
|
# Run hakurei tests on sharefs:
|
||||||
|
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")
|
||||||
@@ -35,6 +35,8 @@ type (
|
|||||||
// Container represents a container environment being prepared or run.
|
// Container represents a container environment being prepared or run.
|
||||||
// None of [Container] methods are safe for concurrent use.
|
// None of [Container] methods are safe for concurrent use.
|
||||||
Container struct {
|
Container struct {
|
||||||
|
// Whether the container init should stay alive after its parent terminates.
|
||||||
|
AllowOrphan bool
|
||||||
// Cgroup fd, nil to disable.
|
// Cgroup fd, nil to disable.
|
||||||
Cgroup *int
|
Cgroup *int
|
||||||
// ExtraFiles passed through to initial process in the container,
|
// ExtraFiles passed through to initial process in the container,
|
||||||
@@ -253,7 +255,6 @@ func (p *Container) Start() error {
|
|||||||
p.cmd.Dir = fhs.Root
|
p.cmd.Dir = fhs.Root
|
||||||
p.cmd.SysProcAttr = &SysProcAttr{
|
p.cmd.SysProcAttr = &SysProcAttr{
|
||||||
Setsid: !p.RetainSession,
|
Setsid: !p.RetainSession,
|
||||||
Pdeathsig: SIGKILL,
|
|
||||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||||
|
|
||||||
@@ -268,6 +269,9 @@ func (p *Container) Start() error {
|
|||||||
|
|
||||||
UseCgroupFD: p.Cgroup != nil,
|
UseCgroupFD: p.Cgroup != nil,
|
||||||
}
|
}
|
||||||
|
if !p.AllowOrphan {
|
||||||
|
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
|
||||||
|
}
|
||||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||||
}
|
}
|
||||||
|
|||||||
2
dist/install.sh
vendored
2
dist/install.sh
vendored
@@ -2,7 +2,7 @@
|
|||||||
cd "$(dirname -- "$0")" || exit 1
|
cd "$(dirname -- "$0")" || exit 1
|
||||||
|
|
||||||
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
install -vDm0755 "bin/hakurei" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hakurei"
|
||||||
install -vDm0755 "bin/hpkg" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hpkg"
|
install -vDm0755 "bin/sharefs" "${HAKUREI_INSTALL_PREFIX}/usr/bin/sharefs"
|
||||||
|
|
||||||
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
install -vDm4511 "bin/hsu" "${HAKUREI_INSTALL_PREFIX}/usr/bin/hsu"
|
||||||
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
if [ ! -f "${HAKUREI_INSTALL_PREFIX}/etc/hsurc" ]; then
|
||||||
|
|||||||
11
flake.nix
11
flake.nix
@@ -69,6 +69,8 @@
|
|||||||
withRace = true;
|
withRace = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
|
||||||
|
|
||||||
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
|
hpkg = callPackage ./cmd/hpkg/test { inherit system self; };
|
||||||
|
|
||||||
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
|
||||||
@@ -136,6 +138,10 @@
|
|||||||
;
|
;
|
||||||
};
|
};
|
||||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||||
|
sharefs = pkgs.linkFarm "sharefs" {
|
||||||
|
"bin/sharefs" = "${hakurei}/libexec/sharefs";
|
||||||
|
"bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
|
||||||
|
};
|
||||||
|
|
||||||
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
||||||
# go requires XDG_CACHE_HOME for the build cache
|
# go requires XDG_CACHE_HOME for the build cache
|
||||||
@@ -160,7 +166,10 @@
|
|||||||
pkgs = nixpkgsFor.${system};
|
pkgs = nixpkgsFor.${system};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell { buildInputs = hakurei.targetPkgs; };
|
default = pkgs.mkShell {
|
||||||
|
buildInputs = hakurei.targetPkgs;
|
||||||
|
hardeningDisable = [ "fortify" ];
|
||||||
|
};
|
||||||
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
withPackage = pkgs.mkShell { buildInputs = [ hakurei ] ++ hakurei.targetPkgs; };
|
||||||
|
|
||||||
vm =
|
vm =
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ var ErrNotDone = errors.New("did not receive a Core::Done event targeting previo
|
|||||||
const (
|
const (
|
||||||
// syncTimeout is the maximum duration [Core.Sync] is allowed to take before
|
// syncTimeout is the maximum duration [Core.Sync] is allowed to take before
|
||||||
// receiving [CoreDone] or failing.
|
// receiving [CoreDone] or failing.
|
||||||
syncTimeout = 5 * time.Second
|
syncTimeout = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
|
// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -27,10 +26,16 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn is a low level unix socket interface used by [Context].
|
// Conn is a low level unix socket interface used by [Context].
|
||||||
type Conn interface {
|
type Conn interface {
|
||||||
|
// MightBlock informs the implementation that the next call to
|
||||||
|
// Recvmsg or Sendmsg might block. A zero or negative timeout
|
||||||
|
// cancels this behaviour.
|
||||||
|
MightBlock(timeout time.Duration)
|
||||||
|
|
||||||
// Recvmsg calls syscall.Recvmsg on the underlying socket.
|
// Recvmsg calls syscall.Recvmsg on the underlying socket.
|
||||||
Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error)
|
Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error)
|
||||||
|
|
||||||
@@ -138,45 +143,142 @@ func New(conn Conn, props SPADict) (*Context, error) {
|
|||||||
return &ctx, nil
|
return &ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// A SyscallConnCloser is a [syscall.Conn] that implements [io.Closer].
|
// unixConn is an implementation of the [Conn] interface for connections
|
||||||
type SyscallConnCloser interface {
|
// to Unix domain sockets.
|
||||||
syscall.Conn
|
type unixConn struct {
|
||||||
io.Closer
|
fd int
|
||||||
|
|
||||||
|
// Whether creation of a new epoll instance was attempted.
|
||||||
|
epoll bool
|
||||||
|
// File descriptor referring to the new epoll instance.
|
||||||
|
// Valid if epoll is true and epollErr is nil.
|
||||||
|
epollFd int
|
||||||
|
// Error returned by syscall.EpollCreate1.
|
||||||
|
epollErr error
|
||||||
|
// Stores epoll events from the kernel.
|
||||||
|
epollBuf [32]syscall.EpollEvent
|
||||||
|
|
||||||
|
// If non-zero, next call is treated as a blocking call.
|
||||||
|
timeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// A SyscallConn is a [Conn] adapter for [syscall.Conn].
|
// Dial connects to a Unix domain socket described by name.
|
||||||
type SyscallConn struct{ SyscallConnCloser }
|
func Dial(name string) (Conn, error) {
|
||||||
|
if fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC|syscall.SOCK_NONBLOCK, 0); err != nil {
|
||||||
|
return nil, os.NewSyscallError("socket", err)
|
||||||
|
} else if err = syscall.Connect(fd, &syscall.SockaddrUnix{Name: name}); err != nil {
|
||||||
|
_ = syscall.Close(fd)
|
||||||
|
return nil, os.NewSyscallError("connect", err)
|
||||||
|
} else {
|
||||||
|
return &unixConn{fd: fd}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Recvmsg implements [Conn.Recvmsg] via [syscall.Conn.SyscallConn].
|
// MightBlock informs the implementation that the next call
|
||||||
func (conn SyscallConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) {
|
// might block for a non-zero timeout.
|
||||||
var rc syscall.RawConn
|
func (conn *unixConn) MightBlock(timeout time.Duration) {
|
||||||
if rc, err = conn.SyscallConn(); err != nil {
|
if timeout < 0 {
|
||||||
|
timeout = 0
|
||||||
|
}
|
||||||
|
conn.timeout = timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// wantsEpoll is called at the beginning of any method that might use epoll.
|
||||||
|
func (conn *unixConn) wantsEpoll() error {
|
||||||
|
if !conn.epoll {
|
||||||
|
conn.epoll = true
|
||||||
|
conn.epollFd, conn.epollErr = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC)
|
||||||
|
if conn.epollErr == nil {
|
||||||
|
if conn.epollErr = syscall.EpollCtl(conn.epollFd, syscall.EPOLL_CTL_ADD, conn.fd, &syscall.EpollEvent{
|
||||||
|
Events: syscall.EPOLLERR | syscall.EPOLLHUP,
|
||||||
|
Fd: int32(conn.fd),
|
||||||
|
}); conn.epollErr != nil {
|
||||||
|
_ = syscall.Close(conn.epollFd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn.epollErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait waits for a specific I/O event on fd. Caller must arrange for wantsEpoll
|
||||||
|
// to be called somewhere before wait is called.
|
||||||
|
func (conn *unixConn) wait(event uint32) (err error) {
|
||||||
|
if conn.timeout == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(conn.timeout)
|
||||||
|
conn.timeout = 0
|
||||||
|
|
||||||
|
if err = syscall.EpollCtl(conn.epollFd, syscall.EPOLL_CTL_MOD, conn.fd, &syscall.EpollEvent{
|
||||||
|
Events: event | syscall.EPOLLERR | syscall.EPOLLHUP,
|
||||||
|
Fd: int32(conn.fd),
|
||||||
|
}); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if controlErr := rc.Control(func(fd uintptr) {
|
for timeout := deadline.Sub(time.Now()); timeout > 0; timeout = deadline.Sub(time.Now()) {
|
||||||
n, oobn, recvflags, _, err = syscall.Recvmsg(int(fd), p, oob, flags)
|
var n int
|
||||||
}); controlErr != nil && err == nil {
|
if n, err = syscall.EpollWait(conn.epollFd, conn.epollBuf[:], int(timeout/time.Millisecond)); err != nil {
|
||||||
err = controlErr
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch n {
|
||||||
|
case 1: // only the socket fd is ever added
|
||||||
|
if conn.epollBuf[0].Fd != int32(conn.fd) { // unreachable
|
||||||
|
return syscall.ENOTRECOVERABLE
|
||||||
|
}
|
||||||
|
if conn.epollBuf[0].Events&event == event ||
|
||||||
|
conn.epollBuf[0].Events&syscall.EPOLLERR|syscall.EPOLLHUP != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = syscall.ETIME
|
||||||
|
continue
|
||||||
|
|
||||||
|
case 0: // timeout
|
||||||
|
return syscall.ETIMEDOUT
|
||||||
|
|
||||||
|
default: // unreachable
|
||||||
|
return syscall.ENOTRECOVERABLE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sendmsg implements [Conn.Sendmsg] via [syscall.Conn.SyscallConn].
|
// Recvmsg calls syscall.Recvmsg on the underlying socket.
|
||||||
func (conn SyscallConn) Sendmsg(p, oob []byte, flags int) (n int, err error) {
|
func (conn *unixConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) {
|
||||||
var rc syscall.RawConn
|
if err = conn.wantsEpoll(); err != nil {
|
||||||
if rc, err = conn.SyscallConn(); err != nil {
|
return
|
||||||
|
} else if err = conn.wait(syscall.EPOLLIN); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if controlErr := rc.Control(func(fd uintptr) {
|
n, oobn, recvflags, _, err = syscall.Recvmsg(conn.fd, p, oob, flags)
|
||||||
n, err = syscall.SendmsgN(int(fd), p, oob, nil, flags)
|
|
||||||
}); controlErr != nil && err == nil {
|
|
||||||
err = controlErr
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sendmsg calls syscall.Sendmsg on the underlying socket.
|
||||||
|
func (conn *unixConn) Sendmsg(p, oob []byte, flags int) (n int, err error) {
|
||||||
|
if err = conn.wantsEpoll(); err != nil {
|
||||||
|
return
|
||||||
|
} else if err = conn.wait(syscall.EPOLLOUT); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = syscall.SendmsgN(conn.fd, p, oob, nil, flags)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying socket and the epoll fd if populated.
|
||||||
|
func (conn *unixConn) Close() (err error) {
|
||||||
|
if conn.epoll && conn.epollErr == nil {
|
||||||
|
conn.epollErr = syscall.Close(conn.epollFd)
|
||||||
|
}
|
||||||
|
if err = syscall.Close(conn.fd); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return conn.epollErr
|
||||||
|
}
|
||||||
|
|
||||||
// MustNew calls [New](conn, props) and panics on error.
|
// MustNew calls [New](conn, props) and panics on error.
|
||||||
// It is intended for use in tests with hard-coded strings.
|
// It is intended for use in tests with hard-coded strings.
|
||||||
func MustNew(conn Conn, props SPADict) *Context {
|
func MustNew(conn Conn, props SPADict) *Context {
|
||||||
@@ -310,7 +412,7 @@ func (ctx *Context) recvmsg(remaining []byte) (payload []byte, err error) {
|
|||||||
}
|
}
|
||||||
if err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
if err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
||||||
ctx.closeReceivedFiles()
|
ctx.closeReceivedFiles()
|
||||||
return nil, os.NewSyscallError("recvmsg", err)
|
return nil, &ProxyFatalError{Err: os.NewSyscallError("recvmsg", err), ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +449,7 @@ func (ctx *Context) sendmsg(p []byte, fds ...int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
if err != nil && err != syscall.EAGAIN && err != syscall.EWOULDBLOCK {
|
||||||
return os.NewSyscallError("sendmsg", err)
|
return &ProxyFatalError{Err: os.NewSyscallError("sendmsg", err), ProxyErrs: ctx.cloneAsProxyErrors()}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -598,8 +700,15 @@ func (ctx *Context) Roundtrip() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// roundtripTimeout is the maximum duration socket operations during
|
||||||
|
// Context.roundtrip is allowed to block for.
|
||||||
|
roundtripTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
// roundtrip implements the Roundtrip method without checking proxyErrors.
|
// roundtrip implements the Roundtrip method without checking proxyErrors.
|
||||||
func (ctx *Context) roundtrip() (err error) {
|
func (ctx *Context) roundtrip() (err error) {
|
||||||
|
ctx.conn.MightBlock(roundtripTimeout)
|
||||||
if err = ctx.sendmsg(ctx.buf, ctx.pendingFiles...); err != nil {
|
if err = ctx.sendmsg(ctx.buf, ctx.pendingFiles...); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -633,6 +742,7 @@ func (ctx *Context) roundtrip() (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var remaining []byte
|
var remaining []byte
|
||||||
|
ctx.conn.MightBlock(roundtripTimeout)
|
||||||
for {
|
for {
|
||||||
remaining, err = ctx.consume(remaining)
|
remaining, err = ctx.consume(remaining)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -857,14 +967,14 @@ const Remote = "PIPEWIRE_REMOTE"
|
|||||||
|
|
||||||
const DEFAULT_SYSTEM_RUNTIME_DIR = "/run/pipewire"
|
const DEFAULT_SYSTEM_RUNTIME_DIR = "/run/pipewire"
|
||||||
|
|
||||||
// connectName connects to a PipeWire remote by name and returns the [net.UnixConn].
|
// connectName connects to a PipeWire remote by name and returns the resulting [Conn].
|
||||||
func connectName(name string, manager bool) (conn *net.UnixConn, err error) {
|
func connectName(name string, manager bool) (conn Conn, err error) {
|
||||||
if manager && !strings.HasSuffix(name, "-manager") {
|
if manager && !strings.HasSuffix(name, "-manager") {
|
||||||
return connectName(name+"-manager", false)
|
return connectName(name+"-manager", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
|
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
|
||||||
return net.DialUnix("unix", nil, &net.UnixAddr{Name: name, Net: "unix"})
|
return Dial(name)
|
||||||
} else {
|
} else {
|
||||||
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
|
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
|
||||||
if !ok || !path.IsAbs(runtimeDir) {
|
if !ok || !path.IsAbs(runtimeDir) {
|
||||||
@@ -879,7 +989,7 @@ func connectName(name string, manager bool) (conn *net.UnixConn, err error) {
|
|||||||
if !ok || !path.IsAbs(runtimeDir) {
|
if !ok || !path.IsAbs(runtimeDir) {
|
||||||
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
|
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
|
||||||
}
|
}
|
||||||
return net.DialUnix("unix", nil, &net.UnixAddr{Name: path.Join(runtimeDir, name), Net: "unix"})
|
return Dial(path.Join(runtimeDir, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,12 +1007,11 @@ func ConnectName(name string, manager bool, props SPADict) (ctx *Context, err er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var conn *net.UnixConn
|
var conn Conn
|
||||||
if conn, err = connectName(name, manager); err != nil {
|
if conn, err = connectName(name, manager); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if ctx, err = New(conn, props); err != nil {
|
||||||
if ctx, err = New(SyscallConn{conn}, props); err != nil {
|
|
||||||
ctx = nil
|
ctx = nil
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
. "syscall"
|
. "syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hakurei.app/container/stub"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/internal/pipewire"
|
"hakurei.app/internal/pipewire"
|
||||||
@@ -715,6 +716,18 @@ type stubUnixConn struct {
|
|||||||
current int
|
current int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conn *stubUnixConn) MightBlock(timeout time.Duration) {
|
||||||
|
if timeout != 5*time.Second {
|
||||||
|
panic("unexpected timeout " + timeout.String())
|
||||||
|
}
|
||||||
|
if conn.current == 0 ||
|
||||||
|
(conn.samples[conn.current-1].nr == SYS_RECVMSG && conn.samples[conn.current-1].errno == EAGAIN && conn.samples[conn.current].nr == SYS_SENDMSG) ||
|
||||||
|
(conn.samples[conn.current-1].nr == SYS_SENDMSG && conn.samples[conn.current].nr == SYS_RECVMSG) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic("unexpected blocking hint before sample " + strconv.Itoa(conn.current))
|
||||||
|
}
|
||||||
|
|
||||||
// nextSample returns the current sample and increments the counter.
|
// nextSample returns the current sample and increments the counter.
|
||||||
func (conn *stubUnixConn) nextSample(nr uintptr) (sample *stubUnixConnSample, wantOOB []byte, err error) {
|
func (conn *stubUnixConn) nextSample(nr uintptr) (sample *stubUnixConnSample, wantOOB []byte, err error) {
|
||||||
sample = &conn.samples[conn.current]
|
sample = &conn.samples[conn.current]
|
||||||
|
|||||||
307
internal/pkg/pkg.go
Normal file
307
internal/pkg/pkg.go
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
// Package pkg provides utilities for packaging software.
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// A Checksum is a SHA-384 checksum computed for a cured [Artifact].
|
||||||
|
Checksum = [sha512.Size384]byte
|
||||||
|
|
||||||
|
// An ID is a unique identifier returned by [Artifact.ID]. This value must
|
||||||
|
// be deterministically determined ahead of time.
|
||||||
|
ID Checksum
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustDecode decodes a string representation of [Checksum] and panics if there
|
||||||
|
// is a decoding error or the resulting data is too short.
|
||||||
|
func MustDecode(s string) (checksum Checksum) {
|
||||||
|
if n, err := base64.URLEncoding.Decode(
|
||||||
|
checksum[:],
|
||||||
|
[]byte(s),
|
||||||
|
); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else if n != len(Checksum{}) {
|
||||||
|
panic(io.ErrUnexpectedEOF)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Artifact is a read-only reference to a piece of data that may be created
|
||||||
|
// deterministically but might not currently be available in memory or on the
|
||||||
|
// filesystem.
|
||||||
|
type Artifact interface {
|
||||||
|
// ID returns a globally unique identifier referring to the current
|
||||||
|
// [Artifact]. This value must be known ahead of time and guaranteed to be
|
||||||
|
// unique without having obtained the full contents of the [Artifact].
|
||||||
|
ID() ID
|
||||||
|
|
||||||
|
// Hash returns the [Checksum] created from the full contents of a cured
|
||||||
|
// [Artifact]. This can be stored for future lookup in a [Cache].
|
||||||
|
//
|
||||||
|
// A call to Hash implicitly cures [Artifact].
|
||||||
|
Hash() (Checksum, error)
|
||||||
|
|
||||||
|
// Pathname returns an absolute pathname to a file or directory populated
|
||||||
|
// with the full contents of [Artifact]. This is the most expensive
|
||||||
|
// operation possible on any [Artifact] and should be avoided if possible.
|
||||||
|
//
|
||||||
|
// A call to Pathname implicitly cures [Artifact].
|
||||||
|
//
|
||||||
|
// Callers must only open files read-only. If [Artifact] is a directory,
|
||||||
|
// files must not be created or removed under this directory.
|
||||||
|
Pathname() (*check.Absolute, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A File refers to an [Artifact] backed by a single file.
|
||||||
|
type File interface {
|
||||||
|
// Data returns the full contents of [Artifact].
|
||||||
|
//
|
||||||
|
// Callers must not modify the returned byte slice.
|
||||||
|
Data() ([]byte, error)
|
||||||
|
|
||||||
|
Artifact
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatEntry is the representation of a directory entry via [Flatten].
|
||||||
|
type FlatEntry struct {
|
||||||
|
Name string // base name of the file
|
||||||
|
Mode fs.FileMode // file mode bits
|
||||||
|
Data []byte // file content or symlink destination
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten writes a deterministic representation of the contents of fsys to w.
|
||||||
|
// The resulting data can be hashed to produce a deterministic checksum for the
|
||||||
|
// directory.
|
||||||
|
func Flatten(fsys fs.FS, root string, w io.Writer) error {
|
||||||
|
e := gob.NewEncoder(w)
|
||||||
|
return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fi fs.FileInfo
|
||||||
|
fi, err = d.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ent := FlatEntry{
|
||||||
|
Name: fi.Name(),
|
||||||
|
Mode: fi.Mode(),
|
||||||
|
}
|
||||||
|
if ent.Mode.IsRegular() {
|
||||||
|
if ent.Data, err = fs.ReadFile(fsys, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if ent.Mode&fs.ModeSymlink != 0 {
|
||||||
|
var newpath string
|
||||||
|
if newpath, err = fs.ReadLink(fsys, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ent.Data = []byte(newpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Encode(&ent)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashFS returns a checksum produced by hashing the result of [Flatten].
|
||||||
|
func HashFS(fsys fs.FS, root string) (Checksum, error) {
|
||||||
|
h := sha512.New384()
|
||||||
|
if err := Flatten(fsys, root, h); err != nil {
|
||||||
|
return Checksum{}, err
|
||||||
|
}
|
||||||
|
return (Checksum)(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashDir returns a checksum produced by hashing the result of [Flatten].
|
||||||
|
func HashDir(pathname *check.Absolute) (Checksum, error) {
|
||||||
|
return HashFS(os.DirFS(pathname.String()), ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// dirIdentifier is the directory name appended to Cache.base for storing
|
||||||
|
// artifacts named after their [ID].
|
||||||
|
dirIdentifier = "identifier"
|
||||||
|
// dirChecksum is the directory name appended to Cache.base for storing
|
||||||
|
// artifacts named after their [Checksum].
|
||||||
|
dirChecksum = "checksum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache is a support layer that implementations of [Artifact] can use to store
|
||||||
|
// cured [Artifact] data in a content addressed fashion.
|
||||||
|
type Cache struct {
|
||||||
|
// Directory where all [Cache] related files are placed.
|
||||||
|
base *check.Absolute
|
||||||
|
|
||||||
|
// Synchronises access to public methods.
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile loads the contents of a [File] by its identifier.
|
||||||
|
func (c *Cache) LoadFile(id ID) (
|
||||||
|
pathname *check.Absolute,
|
||||||
|
data []byte,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
pathname = c.base.Append(
|
||||||
|
dirIdentifier,
|
||||||
|
base64.URLEncoding.EncodeToString(id[:]),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
data, err = os.ReadFile(pathname.String())
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ChecksumMismatchError describes an [Artifact] with unexpected content.
|
||||||
|
type ChecksumMismatchError struct {
|
||||||
|
// Actual and expected checksums.
|
||||||
|
Got, Want Checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ChecksumMismatchError) Error() string {
|
||||||
|
return "got " + base64.URLEncoding.EncodeToString(e.Got[:]) +
|
||||||
|
" instead of " + base64.URLEncoding.EncodeToString(e.Want[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathname returns the content-addressed pathname for a [Checksum].
|
||||||
|
func (c *Cache) pathname(checksum *Checksum) *check.Absolute {
|
||||||
|
return c.base.Append(
|
||||||
|
dirChecksum,
|
||||||
|
base64.URLEncoding.EncodeToString(checksum[:]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathnameIdent returns the identifier-based pathname for an [ID].
|
||||||
|
func (c *Cache) pathnameIdent(id *ID) *check.Absolute {
|
||||||
|
return c.base.Append(
|
||||||
|
dirIdentifier,
|
||||||
|
base64.URLEncoding.EncodeToString(id[:]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// storeFile stores the contents of a [File]. An optional checksum can be
|
||||||
|
// passed via the result buffer which is used to validate the submitted data.
|
||||||
|
//
|
||||||
|
// If locking is disabled, the caller is responsible for acquiring a write lock
|
||||||
|
// and releasing it after this method returns. This makes LoadOrStoreFile
|
||||||
|
// possible without holding the lock while computing hash for store only.
|
||||||
|
func (c *Cache) storeFile(
|
||||||
|
identifierPathname *check.Absolute,
|
||||||
|
data []byte,
|
||||||
|
buf *Checksum,
|
||||||
|
validate, lock bool,
|
||||||
|
) error {
|
||||||
|
h := sha512.New384()
|
||||||
|
h.Write(data)
|
||||||
|
if validate {
|
||||||
|
if got := (Checksum)(h.Sum(nil)); got != *buf {
|
||||||
|
return &ChecksumMismatchError{got, *buf}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.Sum(buf[:0])
|
||||||
|
}
|
||||||
|
|
||||||
|
checksumPathname := c.pathname(buf)
|
||||||
|
|
||||||
|
if lock {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, err := os.OpenFile(
|
||||||
|
checksumPathname.String(),
|
||||||
|
os.O_WRONLY|os.O_CREATE|os.O_EXCL,
|
||||||
|
0400,
|
||||||
|
); err != nil {
|
||||||
|
// two artifacts may be backed by the same file
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if _, err = f.Write(data); err != nil {
|
||||||
|
// do not attempt cleanup: this is content-addressed and a partial
|
||||||
|
// write is caught during integrity check
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Link(
|
||||||
|
checksumPathname.String(),
|
||||||
|
identifierPathname.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreFile stores the contents of a [File]. An optional checksum can be
|
||||||
|
// passed via the result buffer which is used to validate the submitted data.
|
||||||
|
func (c *Cache) StoreFile(
|
||||||
|
id ID,
|
||||||
|
data []byte,
|
||||||
|
buf *Checksum,
|
||||||
|
validate bool,
|
||||||
|
) (pathname *check.Absolute, err error) {
|
||||||
|
pathname = c.pathnameIdent(&id)
|
||||||
|
err = c.storeFile(pathname, data, buf, validate, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOrStoreFile attempts to load the contents of a [File] by its identifier,
|
||||||
|
// and if that file is not present, calls makeData and stores its result
|
||||||
|
// instead. Hash validation behaviour is identical to StoreFile.
|
||||||
|
func (c *Cache) LoadOrStoreFile(
|
||||||
|
id ID,
|
||||||
|
makeData func() ([]byte, error),
|
||||||
|
buf *Checksum,
|
||||||
|
validate bool,
|
||||||
|
) (
|
||||||
|
pathname *check.Absolute,
|
||||||
|
data []byte,
|
||||||
|
store bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
pathname = c.pathnameIdent(&id)
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
data, err = os.ReadFile(pathname.String())
|
||||||
|
if err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store = true
|
||||||
|
|
||||||
|
data, err = makeData()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.storeFile(pathname, data, buf, validate, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns the address to a new instance of [Cache].
|
||||||
|
func New(base *check.Absolute) (*Cache, error) {
|
||||||
|
for _, name := range []string{
|
||||||
|
dirIdentifier,
|
||||||
|
dirChecksum,
|
||||||
|
} {
|
||||||
|
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
|
||||||
|
!errors.Is(err, os.ErrExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Cache{
|
||||||
|
base: base,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
366
internal/pkg/pkg_test.go
Normal file
366
internal/pkg/pkg_test.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
package pkg_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/base64"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"hakurei.app/container"
|
||||||
|
"hakurei.app/container/check"
|
||||||
|
"hakurei.app/container/stub"
|
||||||
|
"hakurei.app/internal/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCache(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const testdata = "" +
|
||||||
|
"\x00\x00\x00\x00" +
|
||||||
|
"\xad\x0b\x00" +
|
||||||
|
"\x04" +
|
||||||
|
"\xfe\xfe\x00\x00" +
|
||||||
|
"\xfe\xca\x00\x00"
|
||||||
|
|
||||||
|
testdataChecksum := func() pkg.Checksum {
|
||||||
|
h := sha512.New384()
|
||||||
|
h.Write([]byte(testdata))
|
||||||
|
return (pkg.Checksum)(h.Sum(nil))
|
||||||
|
}()
|
||||||
|
|
||||||
|
testdataChecksumString := base64.URLEncoding.EncodeToString(testdataChecksum[:])
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
early func(t *testing.T, base *check.Absolute)
|
||||||
|
f func(t *testing.T, base *check.Absolute, c *pkg.Cache)
|
||||||
|
check func(t *testing.T, base *check.Absolute)
|
||||||
|
}{
|
||||||
|
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||||
|
wantErrNonexistent := &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: base.Append(
|
||||||
|
"identifier",
|
||||||
|
testdataChecksumString,
|
||||||
|
).String(),
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if _, _, err := c.LoadFile(testdataChecksum); !reflect.DeepEqual(err, wantErrNonexistent) {
|
||||||
|
t.Fatalf("LoadFile: error = %#v, want %#v", err, wantErrNonexistent)
|
||||||
|
}
|
||||||
|
|
||||||
|
identifier := (pkg.ID)(bytes.Repeat([]byte{
|
||||||
|
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
|
||||||
|
}, 8))
|
||||||
|
wantPathname := base.Append(
|
||||||
|
"identifier",
|
||||||
|
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||||
|
)
|
||||||
|
identifier0 := (pkg.ID)(bytes.Repeat([]byte{
|
||||||
|
0x71, 0xa7, 0xde, 0x6d, 0xa6, 0xde,
|
||||||
|
}, 8))
|
||||||
|
wantPathname0 := base.Append(
|
||||||
|
"identifier",
|
||||||
|
"cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe",
|
||||||
|
)
|
||||||
|
|
||||||
|
// initial store
|
||||||
|
if pathname, err := c.StoreFile(
|
||||||
|
identifier,
|
||||||
|
[]byte(testdata),
|
||||||
|
&testdataChecksum,
|
||||||
|
true,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("StoreFile: error = %v", err)
|
||||||
|
} else if !pathname.Is(wantPathname) {
|
||||||
|
t.Fatalf("StoreFile: pathname = %q, want %q", pathname, wantPathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load or store, identical content
|
||||||
|
if pathname, data, store, err := c.LoadOrStoreFile(identifier0, func() ([]byte, error) {
|
||||||
|
return []byte(testdata), nil
|
||||||
|
}, &testdataChecksum, true); err != nil {
|
||||||
|
t.Fatalf("LoadOrStoreFile: error = %v", err)
|
||||||
|
} else if !pathname.Is(wantPathname0) {
|
||||||
|
t.Fatalf("LoadOrStoreFile: pathname = %q, want %q", pathname, wantPathname0)
|
||||||
|
} else if string(data) != testdata {
|
||||||
|
t.Fatalf("LoadOrStoreFile: data = %x, want %x", data, testdata)
|
||||||
|
} else if !store {
|
||||||
|
t.Fatal("LoadOrStoreFile did not store nonpresent entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load or store, existing entry
|
||||||
|
if pathname, data, store, err := c.LoadOrStoreFile(identifier, func() ([]byte, error) {
|
||||||
|
return []byte(testdata), nil
|
||||||
|
}, &testdataChecksum, true); err != nil {
|
||||||
|
t.Fatalf("LoadOrStoreFile: error = %v", err)
|
||||||
|
} else if !pathname.Is(wantPathname) {
|
||||||
|
t.Fatalf("LoadOrStoreFile: pathname = %q, want %q", pathname, wantPathname)
|
||||||
|
} else if string(data) != testdata {
|
||||||
|
t.Fatalf("LoadOrStoreFile: data = %x, want %x", data, testdata)
|
||||||
|
} else if store {
|
||||||
|
t.Fatal("LoadOrStoreFile stored over present entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// load, existing entry
|
||||||
|
if pathname, data, err := c.LoadFile(identifier0); err != nil {
|
||||||
|
t.Fatalf("LoadFile: error = %v", err)
|
||||||
|
} else if !pathname.Is(wantPathname0) {
|
||||||
|
t.Fatalf("LoadFile: pathname = %q, want %q", pathname, wantPathname0)
|
||||||
|
} else if string(data) != testdata {
|
||||||
|
t.Fatalf("LoadFile: data = %x, want %x", data, testdata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checksum mismatch
|
||||||
|
wantErrChecksum := &pkg.ChecksumMismatchError{
|
||||||
|
Got: testdataChecksum,
|
||||||
|
}
|
||||||
|
if _, err := c.StoreFile(
|
||||||
|
testdataChecksum,
|
||||||
|
[]byte(testdata),
|
||||||
|
new(pkg.Checksum),
|
||||||
|
true,
|
||||||
|
); !reflect.DeepEqual(err, wantErrChecksum) {
|
||||||
|
t.Fatalf("StoreFile: error = %#v, want %#v", err, wantErrChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify failed store
|
||||||
|
if _, _, err := c.LoadFile(testdataChecksum); !reflect.DeepEqual(err, wantErrNonexistent) {
|
||||||
|
t.Fatalf("LoadFile: error = %#v, want %#v", err, wantErrNonexistent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store, same identifier
|
||||||
|
wantPathnameF := base.Append(
|
||||||
|
"identifier",
|
||||||
|
testdataChecksumString,
|
||||||
|
)
|
||||||
|
if pathname, err := c.StoreFile(
|
||||||
|
testdataChecksum,
|
||||||
|
[]byte(testdata),
|
||||||
|
&testdataChecksum,
|
||||||
|
true,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("StoreFile: error = %v", err)
|
||||||
|
} else if !pathname.Is(wantPathnameF) {
|
||||||
|
t.Fatalf("StoreFile: pathname = %q, want %q", pathname, wantPathnameF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load, same identifier
|
||||||
|
if pathname, data, err := c.LoadFile(testdataChecksum); err != nil {
|
||||||
|
t.Fatalf("LoadFile: error = %v", err)
|
||||||
|
} else if !pathname.Is(wantPathnameF) {
|
||||||
|
t.Fatalf("LoadFile: pathname = %q, want %q", pathname, wantPathnameF)
|
||||||
|
} else if string(data) != testdata {
|
||||||
|
t.Fatalf("LoadFile: data = %x, want %x", data, testdata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store without validation
|
||||||
|
wantChecksum := pkg.Checksum{
|
||||||
|
0xbe, 0xc0, 0x21, 0xb4, 0xf3, 0x68,
|
||||||
|
0xe3, 0x06, 0x91, 0x34, 0xe0, 0x12,
|
||||||
|
0xc2, 0xb4, 0x30, 0x70, 0x83, 0xd3,
|
||||||
|
0xa9, 0xbd, 0xd2, 0x06, 0xe2, 0x4e,
|
||||||
|
0x5f, 0x0d, 0x86, 0xe1, 0x3d, 0x66,
|
||||||
|
0x36, 0x65, 0x59, 0x33, 0xec, 0x2b,
|
||||||
|
0x41, 0x34, 0x65, 0x96, 0x68, 0x17,
|
||||||
|
0xa9, 0xc2, 0x08, 0xa1, 0x17, 0x17,
|
||||||
|
}
|
||||||
|
var gotChecksum pkg.Checksum
|
||||||
|
wantPathnameG := base.Append(
|
||||||
|
"identifier",
|
||||||
|
base64.URLEncoding.EncodeToString(wantChecksum[:]),
|
||||||
|
)
|
||||||
|
if pathname, err := c.StoreFile(
|
||||||
|
wantChecksum,
|
||||||
|
[]byte{0},
|
||||||
|
&gotChecksum,
|
||||||
|
false,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("StoreFile: error = %#v", err)
|
||||||
|
} else if !pathname.Is(wantPathnameG) {
|
||||||
|
t.Fatalf("StoreFile: pathname = %q, want %q", pathname, wantPathnameG)
|
||||||
|
} else if gotChecksum != wantChecksum {
|
||||||
|
t.Fatalf("StoreFile: buf = %x, want %x", gotChecksum, wantChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeData passthrough
|
||||||
|
var zeroIdent pkg.ID
|
||||||
|
wantErrPassthrough := stub.UniqueError(0xcafe)
|
||||||
|
if _, _, _, err := c.LoadOrStoreFile(zeroIdent, func() ([]byte, error) {
|
||||||
|
return nil, wantErrPassthrough
|
||||||
|
}, new(pkg.Checksum), true); !reflect.DeepEqual(err, wantErrPassthrough) {
|
||||||
|
t.Fatalf("LoadOrStoreFile: error = %#v, want %#v", err, wantErrPassthrough)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify failed store
|
||||||
|
wantErrNonexistentZero := &os.PathError{
|
||||||
|
Op: "open",
|
||||||
|
Path: base.Append(
|
||||||
|
"identifier",
|
||||||
|
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||||
|
).String(),
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if _, _, err := c.LoadFile(zeroIdent); !reflect.DeepEqual(err, wantErrNonexistentZero) {
|
||||||
|
t.Fatalf("LoadFile: error = %#v, want %#v", err, wantErrNonexistentZero)
|
||||||
|
}
|
||||||
|
}, func(t *testing.T, base *check.Absolute) {
|
||||||
|
wantChecksum := pkg.MustDecode(
|
||||||
|
"lvK4lY9bQUFscHpxqHmiPvptjUwOgn3BFhzCXZMeupkY1n22WUPSuh7pswEvVZrx",
|
||||||
|
)
|
||||||
|
if checksum, err := pkg.HashDir(base); err != nil {
|
||||||
|
t.Fatalf("HashDir: error = %v", err)
|
||||||
|
} else if checksum != wantChecksum {
|
||||||
|
t.Fatalf("HashDir: %v", &pkg.ChecksumMismatchError{
|
||||||
|
Got: checksum,
|
||||||
|
Want: wantChecksum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
base := check.MustAbs(t.TempDir())
|
||||||
|
if err := os.Chmod(base.String(), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Chmod(path, 0700)
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if c, err := pkg.New(base); err != nil {
|
||||||
|
t.Fatalf("New: error = %v", err)
|
||||||
|
} else {
|
||||||
|
if tc.early != nil {
|
||||||
|
tc.early(t, base)
|
||||||
|
}
|
||||||
|
tc.f(t, base, c)
|
||||||
|
tc.check(t, base)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFlatten(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
fsys fs.FS
|
||||||
|
want pkg.Checksum
|
||||||
|
}{
|
||||||
|
{"sample cache file", fstest.MapFS{
|
||||||
|
".": {Mode: 020000000700},
|
||||||
|
|
||||||
|
"checksum": {Mode: 020000000700},
|
||||||
|
"checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0x0}},
|
||||||
|
"checksum/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||||
|
|
||||||
|
"identifier": {Mode: 020000000700},
|
||||||
|
"identifier/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX": {Mode: 0400, Data: []byte{0x0}},
|
||||||
|
"identifier/0bSFPu5Tnd-2Jj0Mv6co23PW2t3BmHc7eLFj9TgY3eIBg8zislo7xZYNBqovVLcq": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||||
|
"identifier/cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||||
|
"identifier/deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {Mode: 0400, Data: []byte{0x0, 0x0, 0x0, 0x0, 0xad, 0xb, 0x0, 0x4, 0xfe, 0xfe, 0x0, 0x0, 0xfe, 0xca, 0x0, 0x0}},
|
||||||
|
}, pkg.MustDecode("lvK4lY9bQUFscHpxqHmiPvptjUwOgn3BFhzCXZMeupkY1n22WUPSuh7pswEvVZrx")},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got, err := pkg.HashFS(tc.fsys, "."); err != nil {
|
||||||
|
t.Fatalf("HashFS: error = %v", err)
|
||||||
|
} else if got != tc.want {
|
||||||
|
t.Fatalf("HashFS: %v", &pkg.ChecksumMismatchError{
|
||||||
|
Got: got,
|
||||||
|
Want: tc.want,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"ChecksumMismatchError", &pkg.ChecksumMismatchError{
|
||||||
|
Want: (pkg.Checksum)(bytes.Repeat([]byte{
|
||||||
|
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
|
||||||
|
}, 8)),
|
||||||
|
}, "got AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||||
|
" instead of deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := tc.err.Error(); got != tc.want {
|
||||||
|
t.Errorf("Error: %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("nonexistent", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wantErr := &os.PathError{
|
||||||
|
Op: "mkdir",
|
||||||
|
Path: container.Nonexistent,
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
}
|
||||||
|
if _, err := pkg.New(check.MustAbs(container.Nonexistent)); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Errorf("New: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("permission", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tempDir := check.MustAbs(t.TempDir())
|
||||||
|
if err := os.Chmod(tempDir.String(), 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err = os.Chmod(tempDir.String(), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wantErr := &os.PathError{
|
||||||
|
Op: "mkdir",
|
||||||
|
Path: tempDir.Append("cache").String(),
|
||||||
|
Err: syscall.EACCES,
|
||||||
|
}
|
||||||
|
if _, err := pkg.New(tempDir.Append("cache")); !reflect.DeepEqual(err, wantErr) {
|
||||||
|
t.Errorf("New: error = %#v, want %#v", err, wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"hakurei.app/container/stub"
|
"hakurei.app/container/stub"
|
||||||
"hakurei.app/internal/acl"
|
"hakurei.app/internal/acl"
|
||||||
@@ -497,6 +498,12 @@ type stubPipeWireConn struct {
|
|||||||
curSendmsg int
|
curSendmsg int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conn *stubPipeWireConn) MightBlock(timeout time.Duration) {
|
||||||
|
if timeout != 5*time.Second {
|
||||||
|
panic("unexpected timeout " + timeout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Recvmsg marshals and copies a stubMessage prepared ahead of time.
|
// Recvmsg marshals and copies a stubMessage prepared ahead of time.
|
||||||
func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) {
|
func (conn *stubPipeWireConn) Recvmsg(p, _ []byte, _ int) (n, _, recvflags int, err error) {
|
||||||
defer func() { conn.curRecvmsg++ }()
|
defer func() { conn.curRecvmsg++ }()
|
||||||
|
|||||||
71
nixos.nix
71
nixos.nix
@@ -24,11 +24,38 @@ let
|
|||||||
getsubuid = userid: appid: userid * 100000 + 10000 + appid;
|
getsubuid = userid: appid: userid * 100000 + 10000 + appid;
|
||||||
getsubname = userid: appid: "u${toString userid}_a${toString appid}";
|
getsubname = userid: appid: "u${toString userid}_a${toString appid}";
|
||||||
getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}";
|
getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}";
|
||||||
|
|
||||||
|
mountpoints = {
|
||||||
|
${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) {
|
||||||
|
depends = [ cfg.sharefs.source ];
|
||||||
|
device = "sharefs";
|
||||||
|
fsType = "fuse.sharefs";
|
||||||
|
noCheck = true;
|
||||||
|
options = [
|
||||||
|
"rw"
|
||||||
|
"noexec"
|
||||||
|
"nosuid"
|
||||||
|
"nodev"
|
||||||
|
"noatime"
|
||||||
|
"allow_other"
|
||||||
|
"mkdir"
|
||||||
|
"source=${cfg.sharefs.source}"
|
||||||
|
"setuid=${toString config.users.users.${cfg.sharefs.user}.uid}"
|
||||||
|
"setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
in
|
in
|
||||||
|
|
||||||
{
|
{
|
||||||
imports = [ (import ./options.nix packages) ];
|
imports = [ (import ./options.nix packages) ];
|
||||||
|
|
||||||
|
options = {
|
||||||
|
# Forward declare a dummy option for VM filesystems since the real one won't exist
|
||||||
|
# unless the VM module is actually imported.
|
||||||
|
virtualisation.fileSystems = lib.mkOption { };
|
||||||
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
assertions = [
|
assertions = [
|
||||||
(
|
(
|
||||||
@@ -66,6 +93,10 @@ in
|
|||||||
) "" cfg.users;
|
) "" cfg.users;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package;
|
||||||
|
fileSystems = mountpoints;
|
||||||
|
virtualisation.fileSystems = mountpoints;
|
||||||
|
|
||||||
home-manager =
|
home-manager =
|
||||||
let
|
let
|
||||||
privPackages = mapAttrs (_: userid: {
|
privPackages = mapAttrs (_: userid: {
|
||||||
@@ -322,25 +353,57 @@ in
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
users = mkMerge (
|
users = mkMerge (
|
||||||
foldlAttrs (
|
foldlAttrs
|
||||||
|
(
|
||||||
acc: _: fid:
|
acc: _: fid:
|
||||||
acc
|
acc
|
||||||
++ foldlAttrs (
|
++ foldlAttrs (
|
||||||
acc': _: app:
|
acc': _: app:
|
||||||
acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ]
|
acc' ++ [ { ${getsubname fid app.identity} = getuser fid app.identity; } ]
|
||||||
) [ { ${getsubname fid 0} = getuser fid 0; } ] cfg.apps
|
) [ { ${getsubname fid 0} = getuser fid 0; } ] cfg.apps
|
||||||
) [ ] cfg.users
|
)
|
||||||
|
(
|
||||||
|
if (cfg.sharefs.source != null) then
|
||||||
|
[
|
||||||
|
{
|
||||||
|
${cfg.sharefs.user} = {
|
||||||
|
uid = lib.mkDefault 1023;
|
||||||
|
inherit (cfg.sharefs) group;
|
||||||
|
isSystemUser = true;
|
||||||
|
home = cfg.sharefs.source;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
)
|
||||||
|
cfg.users
|
||||||
);
|
);
|
||||||
|
|
||||||
groups = mkMerge (
|
groups = mkMerge (
|
||||||
foldlAttrs (
|
foldlAttrs
|
||||||
|
(
|
||||||
acc: _: fid:
|
acc: _: fid:
|
||||||
acc
|
acc
|
||||||
++ foldlAttrs (
|
++ foldlAttrs (
|
||||||
acc': _: app:
|
acc': _: app:
|
||||||
acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ]
|
acc' ++ [ { ${getsubname fid app.identity} = getgroup fid app.identity; } ]
|
||||||
) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps
|
) [ { ${getsubname fid 0} = getgroup fid 0; } ] cfg.apps
|
||||||
) [ ] cfg.users
|
)
|
||||||
|
(
|
||||||
|
if (cfg.sharefs.source != null) then
|
||||||
|
[
|
||||||
|
{
|
||||||
|
${cfg.sharefs.group} = {
|
||||||
|
gid = lib.mkDefault 1023;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
)
|
||||||
|
cfg.users
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
90
options.md
90
options.md
@@ -809,6 +809,96 @@ package
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## environment\.hakurei\.sharefs\.package
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
The sharefs package to use\.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Type:*
|
||||||
|
package
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Default:*
|
||||||
|
` <derivation sharefs> `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## environment\.hakurei\.sharefs\.group
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Name of the group to run the sharefs daemon as\.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Type:*
|
||||||
|
string
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Default:*
|
||||||
|
` "sharefs" `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## environment\.hakurei\.sharefs\.name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Host path to mount sharefs on\.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Type:*
|
||||||
|
string
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Default:*
|
||||||
|
` "/sdcard" `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## environment\.hakurei\.sharefs\.source
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Writable backing directory\. Setting this to null disables sharefs\.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Type:*
|
||||||
|
null or string
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Default:*
|
||||||
|
` null `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## environment\.hakurei\.sharefs\.user
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Name of the user to run the sharefs daemon as\.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Type:*
|
||||||
|
string
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Default:*
|
||||||
|
` "sharefs" `
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## environment\.hakurei\.shell
|
## environment\.hakurei\.shell
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
40
options.nix
40
options.nix
@@ -40,6 +40,46 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sharefs = {
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = packages.${pkgs.stdenv.hostPlatform.system}.sharefs;
|
||||||
|
description = "The sharefs package to use.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "sharefs";
|
||||||
|
description = ''
|
||||||
|
Name of the user to run the sharefs daemon as.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "sharefs";
|
||||||
|
description = ''
|
||||||
|
Name of the group to run the sharefs daemon as.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "/sdcard";
|
||||||
|
description = ''
|
||||||
|
Host path to mount sharefs on.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
source = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Writable backing directory. Setting this to null disables sharefs.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
apps = mkOption {
|
apps = mkOption {
|
||||||
type =
|
type =
|
||||||
let
|
let
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
wayland-scanner,
|
wayland-scanner,
|
||||||
xorg,
|
xorg,
|
||||||
|
|
||||||
|
# for sharefs
|
||||||
|
fuse3,
|
||||||
|
|
||||||
# for hpkg
|
# for hpkg
|
||||||
zstd,
|
zstd,
|
||||||
gnutar,
|
gnutar,
|
||||||
@@ -92,6 +95,7 @@ buildGoModule rec {
|
|||||||
buildInputs = [
|
buildInputs = [
|
||||||
libffi
|
libffi
|
||||||
libseccomp
|
libseccomp
|
||||||
|
fuse3
|
||||||
acl
|
acl
|
||||||
wayland
|
wayland
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{ pkgs, ... }:
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
environment.hakurei = {
|
environment.hakurei = rec {
|
||||||
enable = true;
|
enable = true;
|
||||||
stateDir = "/var/lib/hakurei";
|
stateDir = "/var/lib/hakurei";
|
||||||
|
sharefs.source = "${stateDir}/sdcard";
|
||||||
users.alice = 0;
|
users.alice = 0;
|
||||||
apps = {
|
apps = {
|
||||||
"cat.gensokyo.extern.foot.noEnablements" = {
|
"cat.gensokyo.extern.foot.noEnablements" = {
|
||||||
|
|||||||
Reference in New Issue
Block a user