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 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. // databaseFiles are the scan's own database files, which the walk // passes over (see databaseFiles in db.go). type walkRules struct { excludes []string nonPersistent map[uint64]string databaseFiles []fileRec } // 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() }