scan: skip OS-appropriate default excludes (/proc, /sys, /tmp, /run, /var/tmp on Linux) (closes #124)
check / check (push) Canceled after 0s

Below an operand, the walk no longer enters the default excludes: on
Linux /proc, /sys, /dev, /run, /tmp, /var/tmp and every mount point of a
kernel or memory-backed filesystem, read once from /proc/self/mountinfo;
on macOS /dev, /private/tmp, /private/var/tmp, /private/var/vm,
/System/Volumes/VM and /System/Volumes/Data. The lists live in the new
exclude.go. An operand at or inside one is still walked, also under
another operand, as with -x. Each skipped directory gets one stderr line
through the warning path, is not counted as skipped, and its records
count as outside the scanned operands. --no-default-excludes turns them
off; scan --help lists them.

Judgement call: an unreadable /proc/self/mountinfo fails the scan.
Judgement call: where filesystems are mounted over one another, the last decides.

Model: opus-5-5
This commit is contained in:
2026-10-16 23:28:07 +00:00
parent 68f6bde5ee
commit 5af53914e8
9 changed files with 693 additions and 120 deletions
+177
View File
@@ -0,0 +1,177 @@
package main
// This file holds the default excludes (README "Rules for the walk"):
// the directories a scan's walk does not enter below an operand, unless
// scan is given --no-default-excludes.
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
)
// The systems that have default excludes, as runtime.GOOS names them.
const (
goosLinux = "linux"
goosDarwin = "darwin"
)
// mountInfoPath is the file in which Linux lists the mounts the reading
// process sees.
const mountInfoPath = "/proc/self/mountinfo"
// The fields of a line of mountInfoPath that kernelMounts reads, counted
// from 0 (see proc(5)): the mount point is the fifth field, and after
// the sixth come optional fields, then a field holding "-", then the
// filesystem type.
const (
mountPointField = 4
optionalFieldsStart = 6
)
// typesPerHelpLine is how many filesystem types the scan command's help
// lists on each line.
const typesPerHelpLine = 6
// errMountInfoLine reports a line of mountInfoPath without the fields
// kernelMounts reads.
var errMountInfoLine = errors.New("unexpected line")
// defaultExcludes returns the directories that are default excludes on
// the system goos names, as runtime.GOOS does: a fixed list on Linux and
// on macOS, and none on any other system. On Linux the mount points
// kernelMounts finds are default excludes as well.
func defaultExcludes(goos string) []string {
switch goos {
case goosLinux:
return []string{"/proc", "/sys", "/dev", "/run", "/tmp", "/var/tmp"}
case goosDarwin:
// A walk of / reaches everything in /System/Volumes/Data through
// the firmlinks, so walking it as well would record each of
// those files twice.
return []string{
"/dev", "/private/tmp", "/private/var/tmp", "/private/var/vm",
"/System/Volumes/VM", "/System/Volumes/Data",
}
default:
return nil
}
}
// kernelFilesystemTypes returns the Linux filesystem types whose mount
// points are default excludes: filesystems that the kernel makes up or
// that are kept in memory.
func kernelFilesystemTypes() []string {
return []string{
"proc", "sysfs", "devtmpfs", "devpts", "tmpfs", "ramfs",
"cgroup", "cgroup2", "debugfs", "tracefs", "securityfs", "pstore",
"bpf", "configfs", "fusectl", "mqueue", "hugetlbfs", "efivarfs",
"binfmt_misc", "autofs", "nsfs", "rpc_pipefs", "selinuxfs",
}
}
// findDefaultExcludes returns the default excludes of the system goos
// names, each with what the scan says about it on the line it prints
// when its walk does not enter it. On Linux it reads the mount points
// from mountInfoPath.
func findDefaultExcludes(goos string) (map[string]string, error) {
excludes := make(map[string]string)
for _, dir := range defaultExcludes(goos) {
excludes[dir] = "default exclude"
}
if goos != goosLinux {
return excludes, nil
}
f, err := os.Open(mountInfoPath)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
mounts, err := kernelMounts(f)
if err != nil {
return nil, fmt.Errorf("read %s: %w", mountInfoPath, err)
}
for dir, fsType := range mounts {
excludes[dir] = "default exclude, " + fsType + " filesystem"
}
return excludes, nil
}
// kernelMounts reads mountinfo, laid out as mountInfoPath is, and
// returns the mount point of every filesystem whose type is one of
// kernelFilesystemTypes, with that type. Where one filesystem is mounted
// over another, a later line names the one on top, so the last line for
// a mount point decides.
func kernelMounts(mountinfo io.Reader) (map[string]string, error) {
types := kernelFilesystemTypes()
// The kernel writes a space, tab, newline or backslash in a mount
// point as an octal escape.
unescape := strings.NewReplacer(
`\040`, " ", `\011`, "\t", `\012`, "\n", `\134`, `\`)
mounts := make(map[string]string)
lines := bufio.NewScanner(mountinfo)
for lines.Scan() {
fields := strings.Fields(lines.Text())
sep := slices.Index(fields, "-")
if sep < optionalFieldsStart || sep+1 >= len(fields) {
return nil, fmt.Errorf("%w: %q", errMountInfoLine, lines.Text())
}
mountPoint := unescape.Replace(fields[mountPointField])
fsType := fields[sep+1]
if slices.Contains(types, fsType) {
mounts[mountPoint] = fsType
} else {
delete(mounts, mountPoint)
}
}
return mounts, lines.Err()
}
// defaultExcludesHelp describes the default excludes of the system goos
// names, for the scan command's help.
func defaultExcludesHelp(goos string) string {
dirs := defaultExcludes(goos)
if len(dirs) == 0 {
return "This system has no default excludes."
}
var help strings.Builder
help.WriteString("Below an operand, the walk does not enter these " +
"directories, the default\nexcludes, unless --no-default-excludes " +
"is given:\n\n")
for _, dir := range dirs {
fmt.Fprintf(&help, " %s\n", dir)
}
if goos == goosLinux {
help.WriteString("\nnor the mount point of any filesystem of " +
"these types:\n\n")
types := kernelFilesystemTypes()
for line := range slices.Chunk(types, typesPerHelpLine) {
fmt.Fprintf(&help, " %s\n", strings.Join(line, ", "))
}
}
return help.String()
}