check / check (push) Canceled after 0s
scan never records or reads anything on a filesystem that is not persistent (tmpfs, proc, sysfs and the like), however it reaches it; no flag turns this off. It notes each such filesystem's device ID as it starts: on Linux from /proc/self/mountinfo, on macOS by lstat of each listed mount point. Below an operand the walk also skips the default excludes, a path list per system (Linux, macOS, and Windows, not used yet); --no-default-excludes turns off only those. A default exclude that is itself an operand gets no skip line. The tests stop at once, saying why, when TMPDIR is not on a persistent filesystem. Judgement call: an unreadable mount list fails the scan, and no flag gets round it. Judgement call: a file on such a filesystem below an operand gets the same line as a directory. Judgement call: such an operand that another operand's walk also reaches is named twice. Unverified: whether macOS's mount list gives the device lstat reports, so macOS keeps lstat, and a hidden mount there can still add another filesystem's device. Unverified: the macOS code is never built or run here; bind mounts need root to test. Model: opus-5-5
193 lines
5.9 KiB
Go
193 lines
5.9 KiB
Go
package main
|
|
|
|
// This file holds two rules for the walk (README "Rules for the walk"):
|
|
// the default excludes, the paths a scan's walk does not enter below an
|
|
// operand unless scan is given --no-default-excludes, and the
|
|
// filesystems that are not persistent, on which the scan never records
|
|
// or reads anything.
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// The systems that have default excludes, as runtime.GOOS names them.
|
|
const (
|
|
goosLinux = "linux"
|
|
goosDarwin = "darwin"
|
|
goosWindows = "windows"
|
|
)
|
|
|
|
// The fields of a line of mountInfoPath that readMountInfo reads,
|
|
// counted from 0 (see proc(5)): the device ID, as major:minor, is the
|
|
// third field, and after the sixth come optional fields, then a field
|
|
// holding "-", then the filesystem type.
|
|
const (
|
|
deviceField = 2
|
|
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
|
|
// readMountInfo reads.
|
|
var errMountInfoLine = errors.New("unexpected line")
|
|
|
|
// walkRules holds the two rules for the walk that a scan sets up as it
|
|
// starts. excludes are the default excludes, or none with
|
|
// --no-default-excludes. nonPersistent maps the device ID of every
|
|
// mounted filesystem that is not persistent to its type; the scan
|
|
// records and reads nothing on one of those, whatever the flags.
|
|
type walkRules struct {
|
|
excludes []string
|
|
nonPersistent map[uint64]string
|
|
}
|
|
|
|
// defaultExcludes returns the default excludes of the system goos
|
|
// names, as runtime.GOOS does: a fixed list on Linux, macOS and
|
|
// Windows, and none on any other system.
|
|
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",
|
|
}
|
|
case goosWindows:
|
|
// Relative to the root of the drive an operand lies on. The
|
|
// first three are the page, swap and hibernation files.
|
|
return []string{
|
|
"pagefile.sys", "swapfile.sys", "hiberfil.sys",
|
|
"System Volume Information", "$Recycle.Bin", `Windows\Temp`,
|
|
`Users\*\AppData\Local\Temp`,
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// isDefaultExclude reports whether path is one of excludes, in which a
|
|
// * matches one path component.
|
|
func isDefaultExclude(path string, excludes []string) bool {
|
|
for _, pattern := range excludes {
|
|
// The patterns are fixed, so none is malformed.
|
|
if ok, _ := filepath.Match(pattern, path); ok {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// nonPersistentFilesystemTypes returns the filesystem types that are
|
|
// not persistent: filesystems that the kernel makes up or that are kept
|
|
// in memory. devfs is the macOS filesystem of /dev; the others are
|
|
// Linux names.
|
|
func nonPersistentFilesystemTypes() []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", "devfs",
|
|
}
|
|
}
|
|
|
|
// nonPersistentLine returns the line the scan prints for path when fi,
|
|
// its lstat, puts it on a filesystem in rules.nonPersistent, and ""
|
|
// when it is on any other.
|
|
func (rules walkRules) nonPersistentLine(path string, fi fs.FileInfo) string {
|
|
dev, ok := deviceOfInfo(fi)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
fsType, found := rules.nonPersistent[dev]
|
|
if !found {
|
|
return ""
|
|
}
|
|
|
|
return fmt.Sprintf("scan: skipped %s (%s, not a persistent filesystem)",
|
|
path, fsType)
|
|
}
|
|
|
|
// readMountInfo reads mountinfo, laid out as mountInfoPath is, and
|
|
// returns the device ID of every mounted filesystem that is not
|
|
// persistent, with its type. The device ID is the one each line gives,
|
|
// not that of the file now at its mount point, so a mount that a later
|
|
// mount hides never adds the device of another filesystem.
|
|
func readMountInfo(mountinfo io.Reader) (map[uint64]string, error) {
|
|
types := nonPersistentFilesystemTypes()
|
|
devices := make(map[uint64]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())
|
|
}
|
|
|
|
var major, minor uint32
|
|
|
|
_, err := fmt.Sscanf(fields[deviceField], "%d:%d", &major, &minor)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %q", errMountInfoLine, lines.Text())
|
|
}
|
|
|
|
fsType := fields[sep+1]
|
|
if slices.Contains(types, fsType) {
|
|
devices[unix.Mkdev(major, minor)] = fsType
|
|
}
|
|
}
|
|
|
|
return devices, lines.Err()
|
|
}
|
|
|
|
// defaultExcludesHelp describes, for the scan command's help, the
|
|
// default excludes of the system goos names and, where the scan lists
|
|
// the mounted filesystems, the types it never records anything on.
|
|
func defaultExcludesHelp(goos string) string {
|
|
var help strings.Builder
|
|
|
|
dirs := defaultExcludes(goos)
|
|
if len(dirs) == 0 {
|
|
help.WriteString("This system has no default excludes.\n")
|
|
} else {
|
|
help.WriteString("Below an operand, the walk does not enter these " +
|
|
"default excludes, unless\n--no-default-excludes is given:\n\n")
|
|
|
|
for _, dir := range dirs {
|
|
fmt.Fprintf(&help, " %s\n", dir)
|
|
}
|
|
}
|
|
|
|
if goos == goosLinux || goos == goosDarwin {
|
|
help.WriteString("\nWith or without --no-default-excludes, the scan " +
|
|
"records nothing on a\nfilesystem that is not persistent, " +
|
|
"one of these types:\n\n")
|
|
|
|
types := nonPersistentFilesystemTypes()
|
|
for line := range slices.Chunk(types, typesPerHelpLine) {
|
|
fmt.Fprintf(&help, " %s\n", strings.Join(line, ", "))
|
|
}
|
|
}
|
|
|
|
return help.String()
|
|
}
|