From 30d7f7406252ee0c465c1c090d8f94cf1561bce1 Mon Sep 17 00:00:00 2001 From: desertwitch <24509509+desertwitch@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:19:48 +0200 Subject: [PATCH 1/5] chore: add missing code comments --- cmd/mount.zipfuse/exec.go | 16 ++++++ cmd/mount.zipfuse/main.go | 6 +++ cmd/mount.zipfuse/main_test.go | 2 +- cmd/mount.zipfuse/util.go | 2 + cmd/zipfuse/main.go | 95 +++++++++++++++++++-------------- cmd/zipfuse/util.go | 15 ++++++ internal/webserver/util.go | 9 ++++ internal/webserver/webserver.go | 9 ++++ 8 files changed, 113 insertions(+), 41 deletions(-) diff --git a/cmd/mount.zipfuse/exec.go b/cmd/mount.zipfuse/exec.go index 95d4466..6990174 100644 --- a/cmd/mount.zipfuse/exec.go +++ b/cmd/mount.zipfuse/exec.go @@ -27,6 +27,7 @@ var ( errMountFailed = errors.New("mount failed") ) +// BuildCommand constructs the full command slice from the parsed mount options. func (mh *mountHelper) BuildCommand() []string { var parts []string @@ -43,6 +44,7 @@ func (mh *mountHelper) BuildCommand() []string { return parts } +// BuildOptions constructs the full options slice from the parsed mount options. func (mh *mountHelper) BuildOptions() []string { var parts []string @@ -67,6 +69,10 @@ func (mh *mountHelper) BuildOptions() []string { return parts } +// Execute handles the execution of the ZipFUSE filesystem binary. +// It handles setting up the environment, forking, and executing the +// filesystem process - waiting for a mount success or failure signal. +// The signaling is realized with an [io.Pipe] to the filesystem binary. func (mh *mountHelper) Execute() error { // We must always set up our "parent" environment first, // because [exec.Command] internally requires a sane $PATH. @@ -122,6 +128,8 @@ Do try to pass "xlog=/full/path/to/writeable/logfile" as a mount option. return nil } +// setupEnvironment establishes the environment for the FUSE mount helper, +// later to be inherited by the executed ZipFUSE filesystem binary itself. func (mh *mountHelper) setupEnvironment() { currentPath := os.Getenv("PATH") additionalPath := "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" @@ -136,6 +144,8 @@ func (mh *mountHelper) setupEnvironment() { } } +// setUID handles the execution of the ZipFUSE filesystem binary under another +// configured user account and fallback if the user account cannot be resolved. func (mh *mountHelper) setUID(spa *syscall.SysProcAttr, cmd *exec.Cmd, cmdArgs []string) (*exec.Cmd, *syscall.SysProcAttr) { home, uid, gid, err := resolveUser(mh.Setuid) if err == nil { @@ -167,6 +177,9 @@ func (mh *mountHelper) setUID(spa *syscall.SysProcAttr, cmd *exec.Cmd, cmdArgs [ return cmd, spa } +// waitForMount takes the read-end of an [io.Pipe] and waits for the mount +// success or failure signal communicated by the ZipFUSE filesystem binary. +// "/proc/self/mountinfo" is also observed under "first-come, first-serve". func (mh *mountHelper) waitForMount(r io.Reader) error { signalDone := mh.waitForSignal(r) @@ -200,6 +213,8 @@ func (mh *mountHelper) waitForMount(r io.Reader) error { } } +// waitForSignal observes the [io.Reader] (as read-end of the [io.Pipe]) +// for the mount success or mount failure signal of the filesystem binary. func (mh *mountHelper) waitForSignal(r io.Reader) <-chan error { signalChan := make(chan error, 1) @@ -251,6 +266,7 @@ func (mh *mountHelper) waitForSignal(r io.Reader) <-chan error { return signalChan } +// checkMountTable checks "/proc/self/mountinfo" for the configured mountpoint. func (mh *mountHelper) checkMountTable() (bool, error) { f, err := os.Open("/proc/self/mountinfo") if err != nil { diff --git a/cmd/mount.zipfuse/main.go b/cmd/mount.zipfuse/main.go index 28351d5..d81599c 100644 --- a/cmd/mount.zipfuse/main.go +++ b/cmd/mount.zipfuse/main.go @@ -51,6 +51,7 @@ var ( // Version is the program version (filled in from the Makefile). Version string + // allowedKeys is a map of known arguments to the ZipFUSE program. allowedKeys = map[string]struct{}{ "fd-cache-bypass": {}, "force-unicode": {}, @@ -70,6 +71,7 @@ var ( } ) +// mountHelper is the principal implementation of the FUSE mount helper. type mountHelper struct { Program string Binary string @@ -82,6 +84,7 @@ type mountHelper struct { Timeout time.Duration } +// newMountHelper parses arguments and returns a new [mountHelper] on success. func newMountHelper(args []string) (*mountHelper, error) { mh := &mountHelper{ Program: args[0], @@ -122,6 +125,7 @@ func newMountHelper(args []string) (*mountHelper, error) { return mh, nil } +// parseOptions parses the mount options from the provided argument slice. func (mh *mountHelper) parseOptions(args []string) error { for i := 0; i < len(args); i++ { //nolint:intrange arg := args[i] @@ -187,6 +191,7 @@ func (mh *mountHelper) parseOptions(args []string) error { return nil } +// deriveTypeFromArg tries to deduct the filesystem type from a "-t" argument. func (mh *mountHelper) deriveTypeFromArg(i *int, args []string) error { *i++ if *i >= len(args) { @@ -206,6 +211,7 @@ func (mh *mountHelper) deriveTypeFromArg(i *int, args []string) error { return nil } +// deriveTypeFromSource tries to deduct the filesystem type from the source. func (mh *mountHelper) deriveTypeFromSource() error { parts := strings.SplitN(mh.Source, "#", 2) diff --git a/cmd/mount.zipfuse/main_test.go b/cmd/mount.zipfuse/main_test.go index cde2dc3..4557209 100644 --- a/cmd/mount.zipfuse/main_test.go +++ b/cmd/mount.zipfuse/main_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -// Expectation: The expected command should be built from the given arguments. +// Expectation: The command slice should be built from the given argument slice. // //nolint:maintidx func Test_MountHelper_BuildCommand_Success(t *testing.T) { diff --git a/cmd/mount.zipfuse/util.go b/cmd/mount.zipfuse/util.go index 7ffdd46..5782a8b 100644 --- a/cmd/mount.zipfuse/util.go +++ b/cmd/mount.zipfuse/util.go @@ -6,6 +6,8 @@ import ( "strconv" ) +// resolveUser is a helper function to resolve a user on the operating system. +// It returns the user's home directory, their UID, their GID, and any errors. func resolveUser(spec string) (string, uint32, uint32, error) { var resolvedUser *user.User diff --git a/cmd/zipfuse/main.go b/cmd/zipfuse/main.go index 0a8bc9c..6c3f175 100644 --- a/cmd/zipfuse/main.go +++ b/cmd/zipfuse/main.go @@ -63,6 +63,7 @@ var ( errPanicRecovered = errors.New("panic recovered") ) +// cliOptions describes all configurables of the command-line interface. type cliOptions struct { allowOther bool dryRun bool @@ -85,6 +86,9 @@ type cliOptions struct { webserverAddr string } +// rootCmd is the principal implementation of the command-line interface. +// It describes all configurables and the invocation of the run() function. +// //nolint:mnd func rootCmd() *cobra.Command { var opts cliOptions @@ -143,6 +147,49 @@ func rootCmd() *cobra.Command { return cmd } +// run is the runtime logic for the program as executed by [cobra.Command]. +// It implements the entire lifetime of the program and the served filesystem. +func run(opts cliOptions) error { + rbuf := logging.NewRingBuffer(opts.ringBufferSize, os.Stderr) + + fsys, err := setupFilesystem(opts, rbuf) + if err != nil { + return fmt.Errorf("failed to setup fs: %w", err) + } + defer fsys.Destroy() + + if opts.dryRun { + return dryWalkFS(fsys) + } + + conn, err := mountFilesystem(opts, fsys) + if err != nil { + return fmt.Errorf("failed to mount fs: %w", err) + } + defer cleanupMount(opts.mountDir, conn, fsys) + + err = notifyMountHelper(nil) + if err != nil { + rbuf.Printf("failed to notify mount helper: %v\n", err) + } + + setupSignalHandlers(fsys, rbuf, opts.mountDir) + wg, errChan := serveFilesystem(conn, fsys, opts.fuseVerbose) + + if opts.webserverAddr != "" { + srv, err := serveDashboard(opts.webserverAddr, fsys, rbuf) + if err != nil { + return fmt.Errorf("failed to setup webserver: %w", err) + } + defer srv.Close() + } + + wg.Wait() + + return <-errChan +} + +// setupFilesystem configures and returns the [filesystem.FS] to be served. func setupFilesystem(opts cliOptions, rbuf *logging.RingBuffer) (*filesystem.FS, error) { fopts := &filesystem.Options{ FDCacheSize: opts.fdCacheSize, @@ -165,6 +212,7 @@ func setupFilesystem(opts cliOptions, rbuf *logging.RingBuffer) (*filesystem.FS, return fsys, nil } +// mountFilesystem opens a new [fuse.Conn] for the specified mountpoint. func mountFilesystem(opts cliOptions, fsys *filesystem.FS) (*fuse.Conn, error) { mountOpts := []fuse.MountOption{ fuse.FSName("zipfuse"), @@ -185,9 +233,13 @@ func mountFilesystem(opts cliOptions, fsys *filesystem.FS) (*fuse.Conn, error) { return conn, nil } +// notifyMountHelper handles piped notifications to the FUSE mount helper. +// // The FUSE mount helper passes the write end of a [os.Pipe] as file descriptor. // We use the pipe to signal either success or failure to the FUSE mount helper. // The argument is the error (or nil = success) that will be sent over the pipe. +// +// If the FUSE mount helper is not involved, a call to this function is a no-op. func notifyMountHelper(e error) error { if mountHelperNotified { return nil @@ -231,6 +283,7 @@ func notifyMountHelper(e error) error { return nil } +// serveFilesystem serves the [filesystem.FS] on the [fuse.Conn] for the mountpoint. func serveFilesystem(conn *fuse.Conn, fsys *filesystem.FS, verbose bool) (*sync.WaitGroup, <-chan error) { var wg sync.WaitGroup errChan := make(chan error, 1) @@ -264,6 +317,7 @@ func serveFilesystem(conn *fuse.Conn, fsys *filesystem.FS, verbose bool) (*sync. return &wg, errChan } +// serveDashboard sets up a [http.Server] and starts serving a [webserver.FSDashboard]. func serveDashboard(addr string, fsys *filesystem.FS, rbuf *logging.RingBuffer) (*http.Server, error) { dashboard, err := webserver.NewFSDashboard(fsys, rbuf, Version) if err != nil { @@ -273,6 +327,7 @@ func serveDashboard(addr string, fsys *filesystem.FS, rbuf *logging.RingBuffer) return dashboard.Serve(addr), nil } +// cleanupMount runs FS cleanup, unmounts and eventually closes the [fuse.Conn]. func cleanupMount(mountDir string, conn *fuse.Conn, fsys *filesystem.FS) { defer conn.Close() defer fuse.Unmount(mountDir) //nolint:errcheck @@ -300,43 +355,3 @@ func main() { os.Exit(1) } } - -func run(opts cliOptions) error { - rbuf := logging.NewRingBuffer(opts.ringBufferSize, os.Stderr) - - fsys, err := setupFilesystem(opts, rbuf) - if err != nil { - return fmt.Errorf("failed to setup fs: %w", err) - } - defer fsys.Destroy() - - if opts.dryRun { - return dryWalkFS(fsys) - } - - conn, err := mountFilesystem(opts, fsys) - if err != nil { - return fmt.Errorf("failed to mount fs: %w", err) - } - defer cleanupMount(opts.mountDir, conn, fsys) - - err = notifyMountHelper(nil) - if err != nil { - rbuf.Printf("failed to notify mount helper: %v\n", err) - } - - setupSignalHandlers(fsys, rbuf, opts.mountDir) - wg, errChan := serveFilesystem(conn, fsys, opts.fuseVerbose) - - if opts.webserverAddr != "" { - srv, err := serveDashboard(opts.webserverAddr, fsys, rbuf) - if err != nil { - return fmt.Errorf("failed to setup webserver: %w", err) - } - defer srv.Close() - } - - wg.Wait() - - return <-errChan -} diff --git a/cmd/zipfuse/util.go b/cmd/zipfuse/util.go index d682db4..878ad39 100644 --- a/cmd/zipfuse/util.go +++ b/cmd/zipfuse/util.go @@ -19,6 +19,9 @@ import ( "golang.org/x/sys/unix" ) +// fdLimits calculates the file descriptor limits for the program. +// The values are derived from the operating system's soft FD limit. +// //nolint:mnd,err113,nonamedreturns func fdLimits() (fsLimit int, cacheLimit int, err error) { var rlim unix.Rlimit @@ -50,6 +53,13 @@ func fdLimits() (fsLimit int, cacheLimit int, err error) { return fsLimit, cacheLimit, nil } +// setupSignalHandlers sets up the listeners for operating system signals. +// +// - SIGTERM or SIGINT (CTRL+C) gracefully unmounts the filesystem +// - SIGUSR1 forces a garbage collection (within Go) +// - SIGUSR2 dumps a diagnostic stacktrace to standard error (stderr) +// +// Unmount failures are handled and the filesystem restored to working order. func setupSignalHandlers(fsys *filesystem.FS, rbuf *logging.RingBuffer, mountDir string) { sig := make(chan os.Signal, 1) signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) @@ -98,6 +108,9 @@ func setupSignalHandlers(fsys *filesystem.FS, rbuf *logging.RingBuffer, mountDir }() } +// dryWalkFS implements the dry-run mode of the program. +// It does a virtual walk of the would-be filesystem, without mounting. +// As the filesystem is walked, all would-be inodes and paths are printed out. func dryWalkFS(fsys *filesystem.FS) error { ctx, cancel := context.WithCancel(context.Background()) @@ -131,6 +144,8 @@ func dryWalkFS(fsys *filesystem.FS) error { } } +// recoverSignalsPanic is a helper function to be used in the signal handlers, +// deferred functions invoke it to recover internally from any goroutine panics. func recoverSignalsPanic() { r := recover() if r != nil { diff --git a/internal/webserver/util.go b/internal/webserver/util.go index 9b042c8..e0ae0c5 100644 --- a/internal/webserver/util.go +++ b/internal/webserver/util.go @@ -8,14 +8,17 @@ import ( "github.com/dustin/go-humanize" ) +// avgMetadataReadTime returns a string of the average metadata read time. func (d *FSDashboard) avgMetadataReadTime() string { return time.Duration(d.fsys.Metrics.TotalMetadataReadTime.Load() / max(1, d.fsys.Metrics.TotalMetadataReadCount.Load())).String() } +// avgExtractTime returns a string of the average extraction time. func (d *FSDashboard) avgExtractTime() string { return time.Duration(d.fsys.Metrics.TotalExtractTime.Load() / max(1, d.fsys.Metrics.TotalExtractCount.Load())).String() } +// avgExtractSpeed returns a string of the average extraction throughput. func (d *FSDashboard) avgExtractSpeed() string { bytes := d.fsys.Metrics.TotalExtractBytes.Load() ns := d.fsys.Metrics.TotalExtractTime.Load() @@ -29,6 +32,7 @@ func (d *FSDashboard) avgExtractSpeed() string { return humanize.IBytes(uint64(bps)) + "/s" } +// totalExtractBytes returns a string of the total extracted bytes. func (d *FSDashboard) totalExtractBytes() string { bytes := d.fsys.Metrics.TotalExtractBytes.Load() @@ -39,6 +43,7 @@ func (d *FSDashboard) totalExtractBytes() string { return humanize.IBytes(uint64(bytes)) } +// totalFDCacheRatio returns a string of the FD cache hit/miss ratio. func (d *FSDashboard) totalFDCacheRatio() string { hits := d.fsys.Metrics.TotalFDCacheHits.Load() misses := d.fsys.Metrics.TotalFDCacheMisses.Load() @@ -53,6 +58,7 @@ func (d *FSDashboard) totalFDCacheRatio() string { return fmt.Sprintf("%.2f%%", perc) } +// streamPoolHitRatio returns a string of the stream pool hit/miss ratio. func (d *FSDashboard) streamPoolHitRatio() string { hits := d.fsys.Metrics.TotalStreamPoolHits.Load() misses := d.fsys.Metrics.TotalStreamPoolMisses.Load() @@ -67,6 +73,7 @@ func (d *FSDashboard) streamPoolHitRatio() string { return fmt.Sprintf("%.2f%%", perc) } +// streamPoolHitAvgSize returns a string of the average stream pool hit size. func (d *FSDashboard) streamPoolHitAvgSize() string { hits := d.fsys.Metrics.TotalStreamPoolHits.Load() hitBytes := d.fsys.Metrics.TotalStreamPoolHitBytes.Load() @@ -80,6 +87,7 @@ func (d *FSDashboard) streamPoolHitAvgSize() string { return humanize.IBytes(uint64(avg)) } +// streamPoolMissAvgSize returns a string of the average stream pool miss size. func (d *FSDashboard) streamPoolMissAvgSize() string { misses := d.fsys.Metrics.TotalStreamPoolMisses.Load() missBytes := d.fsys.Metrics.TotalStreamPoolMissBytes.Load() @@ -93,6 +101,7 @@ func (d *FSDashboard) streamPoolMissAvgSize() string { return humanize.IBytes(uint64(avg)) } +// enabledOrDisabled returns string "Enabled" or "Disabled" based on a boolean. func enabledOrDisabled(v bool) string { if v { return "Enabled" diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go index 0b131aa..b45472c 100644 --- a/internal/webserver/webserver.go +++ b/internal/webserver/webserver.go @@ -76,6 +76,7 @@ func (d *FSDashboard) Serve(addr string) *http.Server { return srv } +// dashboardMux implements all routes served by the dashboard. func (d *FSDashboard) dashboardMux() *mux.Router { mux := mux.NewRouter() @@ -99,6 +100,7 @@ func (d *FSDashboard) dashboardMux() *mux.Router { return mux } +// fsDashboardData describes all data that is served on the [FSDashboard]. type fsDashboardData struct { AllocBytes string `json:"allocBytes"` AvgExtractSpeed string `json:"avgExtractSpeed"` @@ -139,6 +141,7 @@ type fsDashboardData struct { Version string `json:"version"` } +// collectMetrics is the principal method to fetch fresh [fsDashboardData]. func (d *FSDashboard) collectMetrics() fsDashboardData { var m runtime.MemStats runtime.ReadMemStats(&m) @@ -187,6 +190,7 @@ func (d *FSDashboard) collectMetrics() fsDashboardData { } } +// dashboardHandler handles the front-page of the dashboard. func (d *FSDashboard) dashboardHandler(w http.ResponseWriter, _ *http.Request) { data := d.collectMetrics() @@ -196,6 +200,7 @@ func (d *FSDashboard) dashboardHandler(w http.ResponseWriter, _ *http.Request) { } } +// metricsHandler handles the metrics endpoint of the dashboard. func (d *FSDashboard) metricsHandler(w http.ResponseWriter, _ *http.Request) { data := d.collectMetrics() @@ -205,6 +210,7 @@ func (d *FSDashboard) metricsHandler(w http.ResponseWriter, _ *http.Request) { } } +// gcHandler handles the garbage collection endpoint of the dashboard. func (d *FSDashboard) gcHandler(w http.ResponseWriter, _ *http.Request) { runtime.GC() debug.FreeOSMemory() @@ -219,6 +225,7 @@ func (d *FSDashboard) gcHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "GC forced, current heap: %s.\n", humanize.IBytes(m.Alloc)) } +// resetMetricsHandler handles the reset metrics endpoint of the dashboard. func (d *FSDashboard) resetMetricsHandler(w http.ResponseWriter, _ *http.Request) { d.fsys.Metrics.Errors.Store(0) d.fsys.Metrics.TotalOpenedZips.Store(0) @@ -243,6 +250,7 @@ func (d *FSDashboard) resetMetricsHandler(w http.ResponseWriter, _ *http.Request fmt.Fprintln(w, "Metrics reset.") } +// thresholdHandler handles setting the streaming threshold by endpoint. func (d *FSDashboard) thresholdHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -261,6 +269,7 @@ func (d *FSDashboard) thresholdHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Streaming threshold set: %s.\n", humanize.IBytes(val)) } +// booleanHandler handles setting target atomic booleans by endpoint. func (d *FSDashboard) booleanHandler(desc string, target *atomic.Bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) From fff0d6cde2ebe32e065a7264048da96b3b45ac91 Mon Sep 17 00:00:00 2001 From: desertwitch <24509509+desertwitch@users.noreply.github.com> Date: Sat, 18 Oct 2025 06:51:37 +0200 Subject: [PATCH 2/5] feat: default allow-other to true if user is root --- README.md | 2 +- cmd/zipfuse/main.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c192060..4535cde 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ zipfuse [flags] | Flag | Shorthand | Default | Description | |------|-----------|---------|-------------| -| --allow-other `` | -a | false | Allow other system users to access the mounted filesystem. | +| --allow-other `` | -a | true if root; false if not | Allow other system users to access the mounted filesystem. | | --dry-run `` | -d | false | Do not mount; instead print all would-be inodes and paths to standard output. | | --fd-cache-bypass `` | (none) | false | Disable file descriptor caching; open/close a new file descriptor on every single request. | | --fd-cache-size `` | (none) | (70% of `fd-limit`) | Maximum open file descriptors to retain in cache (for more performant re-accessing). | diff --git a/cmd/zipfuse/main.go b/cmd/zipfuse/main.go index 6c3f175..56c819a 100644 --- a/cmd/zipfuse/main.go +++ b/cmd/zipfuse/main.go @@ -31,6 +31,7 @@ import ( "runtime/debug" "strconv" "sync" + "syscall" "time" "bazil.org/fuse" @@ -93,6 +94,12 @@ type cliOptions struct { func rootCmd() *cobra.Command { var opts cliOptions + var allowOther bool + if euid := syscall.Geteuid(); euid == 0 { + // If the executing user is root, default to true. + allowOther = true + } + fsLimit, cacheLimit, err := fdLimits() if err != nil { fsLimit = filesystem.DefaultOptions().FDLimit @@ -132,7 +139,7 @@ func rootCmd() *cobra.Command { cmd.Flags().BoolVar(&opts.forceUnicode, "force-unicode", true, "Unicode (or generated) paths for ZIPs; disabling garbles non-compliant ZIPs") cmd.Flags().BoolVar(&opts.mustCRC32, "must-crc32", false, "Force integrity verification on non-compressed ZIP files also (at performance cost)") cmd.Flags().BoolVar(&opts.strictCache, "strict-cache", false, "Do not treat ZIP files/contents as immutable (non-changing) for caching decisions") - cmd.Flags().BoolVarP(&opts.allowOther, "allow-other", "a", false, "Allow other users to access the filesystem") + cmd.Flags().BoolVarP(&opts.allowOther, "allow-other", "a", allowOther, "Allow other users to access the filesystem") cmd.Flags().BoolVarP(&opts.dryRun, "dry-run", "d", false, "Do not mount, but print all would-be inodes and paths to standard output (stdout)") cmd.Flags().BoolVarP(&opts.flatMode, "flatten-zips", "f", false, "Flatten ZIP-contained subdirectories and their files into one directory per ZIP") cmd.Flags().BoolVarP(&opts.fuseVerbose, "verbose", "v", false, "Print all verbose FUSE communication and diagnostics to standard error (stderr)") From ae96e3f778ad97c2f7c3751b4cca5521e4747472 Mon Sep 17 00:00:00 2001 From: desertwitch <24509509+desertwitch@users.noreply.github.com> Date: Sat, 18 Oct 2025 07:01:01 +0200 Subject: [PATCH 3/5] chore: rename functions for clarity --- internal/filesystem/node_zipdir.go | 8 +-- internal/filesystem/node_zipdir_test.go | 4 +- internal/filesystem/util.go | 16 ++--- internal/filesystem/util_test.go | 94 ++++++++++++------------- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/internal/filesystem/node_zipdir.go b/internal/filesystem/node_zipdir.go index 51e92f9..e7df477 100644 --- a/internal/filesystem/node_zipdir.go +++ b/internal/filesystem/node_zipdir.go @@ -82,7 +82,7 @@ func (z *zipDirNode) readDirAllFlat(_ context.Context) ([]fuse.Dirent, error) { defer zr.Release() //nolint:errcheck for i, f := range zr.File { - normalizedPath := normalizeZipPath(i, f, m.fsys.Options.ForceUnicode) + normalizedPath := zipEntryNormalize(i, f, m.fsys.Options.ForceUnicode) if isDir(f, normalizedPath) { continue @@ -123,7 +123,7 @@ func (z *zipDirNode) lookupFlat(_ context.Context, name string) (fs.Node, error) defer zr.Release() //nolint:errcheck for i, f := range zr.File { - normalizedPath := normalizeZipPath(i, f, m.fsys.Options.ForceUnicode) + normalizedPath := zipEntryNormalize(i, f, m.fsys.Options.ForceUnicode) // Dirent is already normalized and flat, needs checking against that: flatName, ok := flatEntryName(i, normalizedPath) @@ -166,7 +166,7 @@ func (z *zipDirNode) readDirAllNested(_ context.Context) ([]fuse.Dirent, error) defer zr.Release() //nolint:errcheck for i, f := range zr.File { - normalizedPath := normalizeZipPath(i, f, m.fsys.Options.ForceUnicode) + normalizedPath := zipEntryNormalize(i, f, m.fsys.Options.ForceUnicode) // Prefix is already normalized, needs checking against that: if !strings.HasPrefix(normalizedPath, z.prefix) { @@ -226,7 +226,7 @@ func (z *zipDirNode) lookupNested(_ context.Context, name string) (fs.Node, erro fullPath := z.prefix + name for i, f := range zr.File { - normalizedPath := normalizeZipPath(i, f, m.fsys.Options.ForceUnicode) + normalizedPath := zipEntryNormalize(i, f, m.fsys.Options.ForceUnicode) // Dirent is already normalized, needs checking against that: if normalizedPath == fullPath && !isDir(f, normalizedPath) { diff --git a/internal/filesystem/node_zipdir_test.go b/internal/filesystem/node_zipdir_test.go index 8f1a9ad..3e1959f 100644 --- a/internal/filesystem/node_zipdir_test.go +++ b/internal/filesystem/node_zipdir_test.go @@ -161,13 +161,13 @@ func Test_zipDirNode_readDirAllFlat_LeadingSlash_Success(t *testing.T) { require.NoError(t, err) require.Len(t, ent, 2) - name, ok := flatEntryName(0, normalizeZipPath(0, createTestZipFilePtr(t, "/file.txt"), fsys.Options.ForceUnicode)) + name, ok := flatEntryName(0, zipEntryNormalize(0, createTestZipFilePtr(t, "/file.txt"), fsys.Options.ForceUnicode)) require.True(t, ok) require.Equal(t, name, ent[0].Name) require.NotContains(t, name, "/") require.Equal(t, fuse.DT_File, ent[0].Type) - name, ok = flatEntryName(1, normalizeZipPath(1, createTestZipFilePtr(t, "//normal.txt"), fsys.Options.ForceUnicode)) + name, ok = flatEntryName(1, zipEntryNormalize(1, createTestZipFilePtr(t, "//normal.txt"), fsys.Options.ForceUnicode)) require.True(t, ok) require.Equal(t, name, ent[1].Name) require.NotContains(t, name, "/") diff --git a/internal/filesystem/util.go b/internal/filesystem/util.go index b56bbc7..080804f 100644 --- a/internal/filesystem/util.go +++ b/internal/filesystem/util.go @@ -75,17 +75,17 @@ func isDir(f *zip.File, normalizedPath string) bool { return f.FileInfo().IsDir() || strings.HasSuffix(normalizedPath, "/") } -// normalizeZipPath ensures ZIP paths use slashes and removes malformations. +// zipEntryNormalize ensures ZIP paths use slashes and removes malformations. // It also handles non-unicode paths, trying to get the unicode representation // or instead falling back to a generation using ZIP file index and/or hashing. -func normalizeZipPath(index int, f *zip.File, forceUnicode bool) string { +func zipEntryNormalize(index int, f *zip.File, forceUnicode bool) string { var path string var isUnicode bool if utf8.ValidString(f.Name) { path = f.Name isUnicode = true - } else if p, ok := zipUnicodePathFromExtra(f); ok { + } else if p, ok := zipEntryUnicodeFromExtra(f); ok { path = p isUnicode = true } else { @@ -103,17 +103,17 @@ func normalizeZipPath(index int, f *zip.File, forceUnicode bool) string { if !isUnicode && forceUnicode { // We do this here because the function relies on clean "/". - path = zipPathUnicodeFallback(index, path) + path = zipEntryUnicodeFallback(index, path) } return path } -// zipUnicodePathFromExtra tries to parse the Extra field of a [zip.File] +// zipEntryUnicodeFromExtra tries to parse the Extra field of a [zip.File] // for the Unicode path name field which is located with header ID 0x7075. // //nolint:mnd -func zipUnicodePathFromExtra(f *zip.File) (string, bool) { +func zipEntryUnicodeFromExtra(f *zip.File) (string, bool) { extra := f.Extra i := 0 @@ -143,9 +143,9 @@ func zipUnicodePathFromExtra(f *zip.File) (string, bool) { return "", false } -// zipPathUnicodeFallback tries to salvage as much UTF8 of the original ZIP path +// zipEntryUnicodeFallback tries to salvage as much UTF8 of the original ZIP path // as possible, fallback to generation using archive-internal index and hashing. -func zipPathUnicodeFallback(index int, normalizedPath string) string { +func zipEntryUnicodeFallback(index int, normalizedPath string) string { parts := strings.Split(normalizedPath, "/") converted := make([]string, 0, len(parts)) diff --git a/internal/filesystem/util_test.go b/internal/filesystem/util_test.go index 8937384..a1657b5 100644 --- a/internal/filesystem/util_test.go +++ b/internal/filesystem/util_test.go @@ -236,14 +236,14 @@ func Test_isDir_Success(t *testing.T) { f.SetMode(0o755 | os.ModeDir) } - got := isDir(f, normalizeZipPath(0, f, true)) + got := isDir(f, zipEntryNormalize(0, f, true)) require.Equal(t, tt.want, got) }) } } -// Expectation: normalizeZipPath should handle valid UTF-8 paths correctly. -func Test_normalizeZipPath_ValidUTF8_Success(t *testing.T) { +// Expectation: zipEntryNormalize should handle valid UTF-8 paths correctly. +func Test_zipEntryNormalize_ValidUTF8_Success(t *testing.T) { t.Parallel() tests := []struct { @@ -261,14 +261,14 @@ func Test_normalizeZipPath_ValidUTF8_Success(t *testing.T) { t.Run(tt.in, func(t *testing.T) { t.Parallel() f := createTestZipFilePtr(t, tt.in) - got := normalizeZipPath(0, f, true) + got := zipEntryNormalize(0, f, true) require.Equal(t, tt.want, got) }) } } -// Expectation: normalizeZipPath should normalize path separators. -func Test_normalizeZipPath_Separators_Success(t *testing.T) { +// Expectation: zipEntryNormalize should normalize path separators. +func Test_zipEntryNormalize_Separators_Success(t *testing.T) { t.Parallel() corruptBytes := []byte{0xFF, 0xFE, 0xFD} @@ -289,14 +289,14 @@ func Test_normalizeZipPath_Separators_Success(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() f := createTestZipFilePtr(t, tt.in) - got := normalizeZipPath(0, f, true) + got := zipEntryNormalize(0, f, true) require.Equal(t, tt.want, got) }) } } -// Expectation: normalizeZipPath should use Unicode Extra Field when present and UTF-8 is invalid. -func Test_normalizeZipPath_UnicodeExtraField_Success(t *testing.T) { +// Expectation: zipEntryNormalize should use Unicode Extra Field when present and UTF-8 is invalid. +func Test_zipEntryNormalize_UnicodeExtraField_Success(t *testing.T) { t.Parallel() invalidUTF8 := []byte{0xFF, 0xFE, 0xFD} @@ -326,34 +326,34 @@ func Test_normalizeZipPath_UnicodeExtraField_Success(t *testing.T) { }, } - got := normalizeZipPath(0, f, true) + got := zipEntryNormalize(0, f, true) require.Equal(t, unicodePath, got) } -// Expectation: normalizeZipPath should fall back when UTF-8 invalid and no Unicode Extra Field. -func Test_normalizeZipPath_Fallback_Success(t *testing.T) { +// Expectation: zipEntryNormalize should fall back when UTF-8 invalid and no Unicode Extra Field. +func Test_zipEntryNormalize_Fallback_Success(t *testing.T) { t.Parallel() invalidUTF8 := []byte{0xFF, 0xFE, 0xFD} f := createTestZipFilePtr(t, "dir/"+string(invalidUTF8)+".txt") - got := normalizeZipPath(42, f, true) + got := zipEntryNormalize(42, f, true) require.Equal(t, "dir/noutf8_file(42).txt", got) } -// Expectation: normalizeZipPath should not fall back when UTF-8 invalid and no Unicode Extra Field. -func Test_normalizeZipPath_NoFallback_Success(t *testing.T) { +// Expectation: zipEntryNormalize should not fall back when UTF-8 invalid and no Unicode Extra Field. +func Test_zipEntryNormalize_NoFallback_Success(t *testing.T) { t.Parallel() invalidUTF8 := []byte{0xFF, 0xFE, 0xFD} f := createTestZipFilePtr(t, "dir/"+string(invalidUTF8)+".txt") - got := normalizeZipPath(42, f, false) + got := zipEntryNormalize(42, f, false) require.Equal(t, "dir/\xff\xfe\xfd.txt", got) } -// Expectation: zipUnicodePathFromExtra should extract Unicode path from Extra Field. -func Test_zipUnicodePathFromExtra_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFromExtra should extract Unicode path from Extra Field. +func Test_zipEntryUnicodeFromExtra_Success(t *testing.T) { t.Parallel() unicodePath := "日本語ファイル.txt" @@ -371,13 +371,13 @@ func Test_zipUnicodePathFromExtra_Success(t *testing.T) { }, } - path, ok := zipUnicodePathFromExtra(f) + path, ok := zipEntryUnicodeFromExtra(f) require.True(t, ok) require.Equal(t, unicodePath, path) } -// Expectation: zipUnicodePathFromExtra should return false when no Unicode Extra Field present. -func Test_zipUnicodePathFromExtra_NotFound_Error(t *testing.T) { +// Expectation: zipEntryUnicodeFromExtra should return false when no Unicode Extra Field present. +func Test_zipEntryUnicodeFromExtra_NotFound_Error(t *testing.T) { t.Parallel() f := &zip.File{ @@ -386,12 +386,12 @@ func Test_zipUnicodePathFromExtra_NotFound_Error(t *testing.T) { }, } - _, ok := zipUnicodePathFromExtra(f) + _, ok := zipEntryUnicodeFromExtra(f) require.False(t, ok) } -// Expectation: zipUnicodePathFromExtra should handle malformed Extra Field. -func Test_zipUnicodePathFromExtra_Malformed_Error(t *testing.T) { +// Expectation: zipEntryUnicodeFromExtra should handle malformed Extra Field. +func Test_zipEntryUnicodeFromExtra_Malformed_Error(t *testing.T) { t.Parallel() // Malformed: size extends beyond actual data @@ -403,86 +403,86 @@ func Test_zipUnicodePathFromExtra_Malformed_Error(t *testing.T) { }, } - _, ok := zipUnicodePathFromExtra(f) + _, ok := zipEntryUnicodeFromExtra(f) require.False(t, ok) } -// Expectation: zipPathUnicodeFallback should preserve valid UTF-8 components. -func Test_zipPathUnicodeFallback_ValidUTF8Components_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should preserve valid UTF-8 components. +func Test_zipEntryUnicodeFallback_ValidUTF8Components_Success(t *testing.T) { t.Parallel() path := "valid/utf8/path.txt" - result := zipPathUnicodeFallback(5, path) + result := zipEntryUnicodeFallback(5, path) require.Equal(t, "valid/utf8/path.txt", result) } -// Expectation: zipPathUnicodeFallback should generate fallback names for corrupt filenames. -func Test_zipPathUnicodeFallback_CorruptFilename_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should generate fallback names for corrupt filenames. +func Test_zipEntryUnicodeFallback_CorruptFilename_Success(t *testing.T) { t.Parallel() corruptBytes := []byte{0xFF, 0xFE, 0xFD} path := "valid/dir/" + string(corruptBytes) + ".txt" - got := zipPathUnicodeFallback(42, path) + got := zipEntryUnicodeFallback(42, path) require.Equal(t, "valid/dir/noutf8_file(42).txt", got) } -// Expectation: zipPathUnicodeFallback should generate fallback names for corrupt directories. -func Test_zipPathUnicodeFallback_CorruptDirectory_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should generate fallback names for corrupt directories. +func Test_zipEntryUnicodeFallback_CorruptDirectory_Success(t *testing.T) { t.Parallel() corruptBytes := []byte{0xFF, 0xFE} path := string(corruptBytes) + "/file.txt" - result := zipPathUnicodeFallback(10, path) + result := zipEntryUnicodeFallback(10, path) require.Contains(t, result, "noutf8_dir") require.Contains(t, result, "/file.txt") } -// Expectation: zipPathUnicodeFallback should generate equal fallback names when called twice. -func Test_zipPathUnicodeFallback_DeterministicDirectory_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should generate equal fallback names when called twice. +func Test_zipEntryUnicodeFallback_DeterministicDirectory_Success(t *testing.T) { t.Parallel() corruptBytes := []byte{0xFF, 0xFE} path := string(corruptBytes) + "/" - result1 := zipPathUnicodeFallback(10, path) - result2 := zipPathUnicodeFallback(10, path) + result1 := zipEntryUnicodeFallback(10, path) + result2 := zipEntryUnicodeFallback(10, path) require.Equal(t, result1, result2) } -// Expectation: zipPathUnicodeFallback should preserve non-corrupt extensions. -func Test_zipPathUnicodeFallback_Extension_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should preserve non-corrupt extensions. +func Test_zipEntryUnicodeFallback_Extension_Success(t *testing.T) { t.Parallel() corruptBytes := []byte{0xFF, 0xFE} path := "dir/" + string(corruptBytes) + ".jpg" - result := zipPathUnicodeFallback(7, path) + result := zipEntryUnicodeFallback(7, path) require.Equal(t, "dir/noutf8_file(7).jpg", result) } -// Expectation: zipPathUnicodeFallback should clear suspicious extensions. -func Test_zipPathUnicodeFallback_SuspiciousExtension_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should clear suspicious extensions. +func Test_zipEntryUnicodeFallback_SuspiciousExtension_Success(t *testing.T) { t.Parallel() corruptBytes := []byte{0xFF, 0xFE} path := "dir/" + string(corruptBytes) + "." + string(corruptBytes) - result := zipPathUnicodeFallback(7, path) + result := zipEntryUnicodeFallback(7, path) require.Equal(t, "dir/noutf8_file(7)", result) } -// Expectation: zipPathUnicodeFallback should handle mixed valid and invalid components. -func Test_zipPathUnicodeFallback_MixedComponents_Success(t *testing.T) { +// Expectation: zipEntryUnicodeFallback should handle mixed valid and invalid components. +func Test_zipEntryUnicodeFallback_MixedComponents_Success(t *testing.T) { t.Parallel() corruptDir := []byte{0xFF, 0xFE} corruptFile := []byte{0xFD, 0xFC} path := "valid/" + string(corruptDir) + "/subdir/" + string(corruptFile) + ".log" - result := zipPathUnicodeFallback(15, path) + result := zipEntryUnicodeFallback(15, path) require.Contains(t, result, "valid/") require.Contains(t, result, "noutf8_dir") require.Contains(t, result, "/subdir/") From 36abb3d46b189c933388edcd37d7883dc635380c Mon Sep 17 00:00:00 2001 From: desertwitch <24509509+desertwitch@users.noreply.github.com> Date: Sat, 18 Oct 2025 07:02:55 +0200 Subject: [PATCH 4/5] chore: documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4535cde..c6f6940 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ zipfuse [flags] | Flag | Shorthand | Default | Description | |------|-----------|---------|-------------| -| --allow-other `` | -a | true if root; false if not | Allow other system users to access the mounted filesystem. | +| --allow-other `` | -a | (true if root; false if not) | Allow other system users to access the mounted filesystem. | | --dry-run `` | -d | false | Do not mount; instead print all would-be inodes and paths to standard output. | | --fd-cache-bypass `` | (none) | false | Disable file descriptor caching; open/close a new file descriptor on every single request. | | --fd-cache-size `` | (none) | (70% of `fd-limit`) | Maximum open file descriptors to retain in cache (for more performant re-accessing). | From 6e3fff066afbce922957bb33e3bac62d509d8d85 Mon Sep 17 00:00:00 2001 From: desertwitch <24509509+desertwitch@users.noreply.github.com> Date: Sat, 18 Oct 2025 07:06:55 +0200 Subject: [PATCH 5/5] fix(ci): drop allow_other (should be default now) --- .github/workflows/zipfuse-cli.yml | 2 +- .github/workflows/zipfuse-fstab.yml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/zipfuse-cli.yml b/.github/workflows/zipfuse-cli.yml index 538a0e8..3fc9194 100644 --- a/.github/workflows/zipfuse-cli.yml +++ b/.github/workflows/zipfuse-cli.yml @@ -63,7 +63,7 @@ jobs: - name: Mount filesystem run: | - sudo sh -c 'nohup zipfuse source mountpoint --allow-other &>/var/log/zipfuse.log &' + sudo sh -c 'nohup zipfuse source mountpoint &>/var/log/zipfuse.log &' for i in {1..20}; do if mountpoint -q mountpoint; then diff --git a/.github/workflows/zipfuse-fstab.yml b/.github/workflows/zipfuse-fstab.yml index e1c2706..174f78c 100644 --- a/.github/workflows/zipfuse-fstab.yml +++ b/.github/workflows/zipfuse-fstab.yml @@ -25,7 +25,6 @@ jobs: run: | sudo apt-get update sudo apt-get install -y fuse3 - sudo sed -i 's/#user_allow_other/user_allow_other/' /etc/fuse.conf - name: Vendor dependencies run: make vendor @@ -63,7 +62,7 @@ jobs: - name: Mount filesystem run: | - if ! sudo mount -t zipfuse source mountpoint -o allow_other; then + if ! sudo mount -t zipfuse source mountpoint; then echo "ERROR: Filesystem mount has failed" exit 1 fi