Compare commits

...

5 Commits

Author SHA1 Message Date
brent saner
c6efc2d83c
v1.12.0
FIXED:
* paths: Async searching works correctly now, and is consolidated to a
  single struct for searching options for async and synchronous
  searches.
2024-11-18 17:36:14 -05:00
brent saner
eefe02afaf
v1.11.0
ADDED:
* fsutils: better/additional fsattrs functionality
* paths: highly filterable filesystem searching
2024-11-16 01:28:24 -05:00
brent saner
b82f0c02ed
v1.10.1
FIX:
* fs.FileMode for object type is 0 for regular files, so an additional
  parameter is needed.
2024-11-12 06:50:44 -05:00
brent saner
903dd00c81
v1.10.0
ADDED:
* paths.SearchFsPaths, which lets a user provide a fairly flexible
  function for searching files/directories/etc.
2024-11-12 06:32:04 -05:00
brent saner
70a88ca8b4
v1.9.0
IMPROVED:
* Removed *BROKEN* dep. lrn2fixurshitk
2024-11-07 04:15:45 -05:00
30 changed files with 1189 additions and 202 deletions

3
.gitignore vendored
View File

@ -29,6 +29,9 @@
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test


# Test file
fsutils/testfile

# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out



1
auger/TODO Normal file
View File

@ -0,0 +1 @@
This module is still under work.

View File

@ -7,3 +7,35 @@ const (
augInclTfm string = "incl" // The transformer keyword for Augeas includes. augInclTfm string = "incl" // The transformer keyword for Augeas includes.
augAppendSuffix string = "[last()+1]" augAppendSuffix string = "[last()+1]"
) )

var (
dstPtrTrue bool = true
dstPtrFalse bool = false
)

var (
// PtrTrue and PtrFalse are convenience references for constructing an AugFlags if needed. It is recommended you do not change these values if you do not like being confused.
PtrTrue *bool = &dstPtrTrue
PtrFalse *bool = &dstPtrFalse
)

/*
IncludeOptNone is the default include recursion option for Aug.RecursiveInclude.
* No special behavior is defined
* All include directives are assumed to refer:
* Explicitly/exclusively to file paths
* That must exist
*/
const IncludeOptNone includeOpt = 0
const (
// IncludeOptNoExist specifies that inclusions are allowed to not exist, otherwise an error will be raised while attempting to parse them.
IncludeOptNoExist includeOpt = 1 << iota
// IncludeOptGlobbing indicates that the inclusion system supports globbing (as supported by (github.com/gobwas/glob).Match).
IncludeOptGlobbing
// IncludeOptRegex indicates that the inclusion system supports matching by regex (as supported by regexp).
IncludeOptRegex
// IncludeOptDirs indicates that the inclusion system supports matching by directory.
IncludeOptDirs
// IncludeOptDirsRecursive indicates that the inclusion system also recurses into subdirectories of matched directories. Only used if IncludeOptDirs is also set.
IncludeOptDirsRecursive
)

View File

@ -6,6 +6,7 @@ import (
`strings` `strings`


`honnef.co/go/augeas` `honnef.co/go/augeas`
`r00t2.io/goutils/bitmask`
) )


/* /*
@ -41,12 +42,17 @@ func NewAugerFromAugeas(orig augeas.Augeas) (aug *Aug) {
} }


/* /*
AugpathToFspath returns the filesystem path from an Augeas path. AugpathToFspath returns the filesystem path (i.e. an existing file) from an Augeas path.


It is *required* and expected that the Augeas standard /files prefix be removed first; It is *required* and expected that the Augeas standard /files prefix be removed first;
if not, it is assumed to be part of the filesystem path. if not, it is assumed to be part of the filesystem path.


If a valid path cannot be determined, fsPath will be empty. If a valid path cannot be determined, fsPath will be empty.

To be clear, a file must exist for fsPath to not be empty;
the way AugpathToFsPath works is it recurses bottom-up a
given path and checks for the existence of a file,
continuing upwards if not found.
*/ */
func AugpathToFspath(augPath string) (fsPath string, err error) { func AugpathToFspath(augPath string) (fsPath string, err error) {


@ -95,3 +101,11 @@ func dedupePaths(new, existing []string) (missing []string) {


return return
} }

// getInclPaths applies path options to inclusions.
func getInclPaths(pathSpec string, inclFlags *bitmask.MaskBit) (fpaths []string, err error) {

// TODO

return
}

View File

@ -12,6 +12,7 @@ import (
`github.com/davecgh/go-spew/spew` `github.com/davecgh/go-spew/spew`
`github.com/google/shlex` `github.com/google/shlex`
`honnef.co/go/augeas` `honnef.co/go/augeas`
`r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths` `r00t2.io/sysutils/paths`
) )


@ -146,10 +147,21 @@ breakCmd:
An error will be returned if augLens is a nonexistent or not-loaded Augeas lens module. An error will be returned if augLens is a nonexistent or not-loaded Augeas lens module.


Depending on how many files there are and whether globs vs. explicit filepaths are included, this may take a while. Depending on how many files there are and whether globs vs. explicit filepaths are included, this may take a while.
*/
func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string) (err error) {


if err = a.addIncl(includeDirective, augLens, fsRoot, nil); err != nil { optFlags may be nil, multiple includeOpt (see the IncludeOpt* constants) as variadic parameters/expanded slice,
bitwise-OR'd together, or multiple non-OR'd and OR'd together (all will be combined to a single value).
*/
func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string, optFlags ...includeOpt) (err error) {

var flags *bitmask.MaskBit = bitmask.NewMaskBit()

if optFlags != nil && len(optFlags) > 0 {
for _, f := range optFlags {
flags.AddFlag(f.toMb())
}
}

if err = a.addIncl(includeDirective, augLens, fsRoot, nil, flags); err != nil {
return return
} }


@ -164,14 +176,16 @@ func (a *Aug) RecursiveInclude(augLens, includeDirective, fsRoot string) (err er
newInclPaths are new filesystem paths/Augeas-compatible glob patterns to load into the filetree and recurse into. newInclPaths are new filesystem paths/Augeas-compatible glob patterns to load into the filetree and recurse into.
They may be nil, especially if the first run. They may be nil, especially if the first run.
*/ */
func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPaths []string) (err error) { func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPaths []string, inclFlags *bitmask.MaskBit) (err error) {


var matches []string // Passed around set of Augeas matches. var matches []string // Passed around set of Augeas matches.
var exists bool // Used to indicate if the include path exists.
var includes []string // Filepath(s)/glob(s) from fetching includeDirective in lensInclPath. These are internal to the application but are recursed. var includes []string // Filepath(s)/glob(s) from fetching includeDirective in lensInclPath. These are internal to the application but are recursed.
var lensInclPath string // The path of the included paths in the tree. These are internal to Augeas, not the application. var lensInclPath string // The path of the included paths in the tree. These are internal to Augeas, not the application.
var appendPath string // The path for new Augeas includes. var appendPath string // The path for new Augeas includes.
var match []string // A placeholder for iterating when populating includes. var match []string // A placeholder for iterating when populating includes.
var fpath string // A placeholder for finding the path of a conf file that contains an includeDirective. var fpath string // A placeholder for finding the path of a conf file that contains an includeDirective.
var normalizedIncludes []string // A temporary slice to hold normalization operations and other dynamic building.
var lensPath string = fmt.Sprintf(augLensTpl, augLens) // The path of the lens (augLens) itself. var lensPath string = fmt.Sprintf(augLensTpl, augLens) // The path of the lens (augLens) itself.
var augErr *augeas.Error = new(augeas.Error) // We use this to skip "nonexistent" lens. var augErr *augeas.Error = new(augeas.Error) // We use this to skip "nonexistent" lens.


@ -193,7 +207,7 @@ func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPa


// First canonize paths. // First canonize paths.
if newInclPaths != nil && len(newInclPaths) > 0 { if newInclPaths != nil && len(newInclPaths) > 0 {
// Existing includes. We don't return on an empty lensInclPath because // Existing includes. We don't return on an empty lensInclPath.
if matches, err = a.aug.Match(lensInclPath); err != nil { if matches, err = a.aug.Match(lensInclPath); err != nil {
if errors.As(err, augErr) && augErr.Code == augeas.NoMatch { if errors.As(err, augErr) && augErr.Code == augeas.NoMatch {
err = nil err = nil
@ -221,6 +235,17 @@ func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPa
// We don't want to bother adding multiple incl's for the same path(s); it can negatively affect Augeas loads. // We don't want to bother adding multiple incl's for the same path(s); it can negatively affect Augeas loads.
newInclPaths = dedupePaths(newInclPaths, matches) newInclPaths = dedupePaths(newInclPaths, matches)


// And then apply things like recursion, globbing, etc.
normalizedIncludes = make([]string, 0, len(newInclPaths))
if inclFlags.HasFlag(IncludeOptGlobbing.toMb()) {
// TODO
/*
if strings.Contains(newInclPaths[idx], "*") {

}
*/
}

// Add the new path(s) as Augeas include entries. // Add the new path(s) as Augeas include entries.
if newInclPaths != nil { if newInclPaths != nil {
for _, fsPath := range newInclPaths { for _, fsPath := range newInclPaths {
@ -285,10 +310,13 @@ func (a *Aug) addIncl(includeDirective, augLens string, fsRoot string, newInclPa
} }


if matches != nil && len(matches) != 0 { if matches != nil && len(matches) != 0 {
if err = a.addIncl(includeDirective, augLens, fsRoot, matches); err != nil { if err = a.addIncl(includeDirective, augLens, fsRoot, matches, inclFlags); err != nil {
return return
} }
} }


// TODO
_, _ = exists, normalizedIncludes

return return
} }

13
auger/funcs_includeopt.go Normal file
View File

@ -0,0 +1,13 @@
package auger

import (
`r00t2.io/goutils/bitmask`
)

// toMb returns a bitmask.MaskBit of this includeOpt.
func (i includeOpt) toMb() (mb bitmask.MaskBit) {

mb = bitmask.MaskBit(i)

return
}

View File

@ -22,3 +22,18 @@ func TestNewAuger(t *testing.T) {


_ = aug _ = aug
} }

func TestRecursiveInclude(t *testing.T) {

var aug *Aug
var err error

if aug, err = NewAuger("/", "", &AugFlags{DryRun: PtrTrue}); err != nil {
t.Fatal(err)
}

// This requires Nginx to be installed and with a particularly complex nested include system.
if err = aug.RecursiveInclude("Nginx", "include", "/etc/nginx"); err != nil {
t.Fatal(err)
}
}

View File

@ -2,8 +2,11 @@ package auger


import ( import (
`honnef.co/go/augeas` `honnef.co/go/augeas`
`r00t2.io/goutils/bitmask`
) )


type includeOpt bitmask.MaskBit

// Aug is a wrapper around (honnef.co/go/)augeas.Augeas. Remember to call Aug.Close(). // Aug is a wrapper around (honnef.co/go/)augeas.Augeas. Remember to call Aug.Close().
type Aug struct { type Aug struct {
aug augeas.Augeas aug augeas.Augeas

View File

@ -3,3 +3,4 @@


It is now its own module: r00t2.io/cryptparse It is now its own module: r00t2.io/cryptparse
*/ */
package cryptparse

View File

@ -159,7 +159,7 @@ func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
var procPath string var procPath string
var exists bool var exists bool


envMap = make(map[string]string, 0) envMap = make(map[string]string)


procPath = fmt.Sprintf("/proc/%v/environ", pid) procPath = fmt.Sprintf("/proc/%v/environ", pid)



View File

@ -13,7 +13,7 @@ func envListToMap(envs []string) (envMap map[string]string) {
var kv []string var kv []string
var k, v string var k, v string


envMap = make(map[string]string, 0) envMap = make(map[string]string)


for _, ev := range envs { for _, ev := range envs {
kv = strings.SplitN(ev, "=", 2) kv = strings.SplitN(ev, "=", 2)

3
fsutils/TODO Normal file
View File

@ -0,0 +1,3 @@
- XATTRS
(see FS_XFLAG_* in fs.h, FS_IOC_FSGETXATTR/FS_IOC_FSSETXATTR)
- fs label, UUID? (fs.h)

View File

@ -1,101 +1,36 @@
package fsutils package fsutils


import (
`github.com/g0rbe/go-chattr`
)

// https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h
const (
SecureDelete uint32 = chattr.FS_SECRM_FL // Secure deletion
UnDelete = chattr.FS_UNRM_FL // Undelete
CompressFile = chattr.FS_COMPR_FL // Compress file
SyncUpdatechattr = chattr.FS_SYNC_FL // Synchronous updates
Immutable = chattr.FS_IMMUTABLE_FL // Immutable file
AppendOnly = chattr.FS_APPEND_FL // Writes to file may only append
NoDumpFile = chattr.FS_NODUMP_FL // Do not dump file
NoUpdateAtime = chattr.FS_NOATIME_FL // Do not update atime
IsDirty = chattr.FS_DIRTY_FL // Nobody knows what this does, lol.
CompressedClusters = chattr.FS_COMPRBLK_FL // One or more compressed clusters
NoCompress = chattr.FS_NOCOMP_FL // Don't compress
EncFile = chattr.FS_ENCRYPT_FL // Encrypted file
BtreeFmt = chattr.FS_BTREE_FL // Btree format dir
HashIdxDir = chattr.FS_INDEX_FL // Hash-indexed directory
AfsDir = chattr.FS_IMAGIC_FL // AFS directory
ReservedExt3 = chattr.FS_JOURNAL_DATA_FL // Reserved for ext3
NoMergeTail = chattr.FS_NOTAIL_FL // File tail should not be merged
DirSync = chattr.FS_DIRSYNC_FL // dirsync behaviour (directories only)
DirTop = chattr.FS_TOPDIR_FL // Top of directory hierarchies
ReservedExt4a = chattr.FS_HUGE_FILE_FL // Reserved for ext4
Extents = chattr.FS_EXTENT_FL // Extents
LargeEaInode = chattr.FS_EA_INODE_FL // Inode used for large EA
ReservedExt4b = chattr.FS_EOFBLOCKS_FL // Reserved for ext4
NoCOWFile = chattr.FS_NOCOW_FL // Do not cow file
ReservedExt4c = chattr.FS_INLINE_DATA_FL // Reserved for ext4
UseParentProjId = chattr.FS_PROJINHERIT_FL // Create with parents projid
ReservedExt2 = chattr.FS_RESERVED_FL // Reserved for ext2 lib
)

var ( var (
// AttrNameValueMap contains a mapping of attribute names (as designated by this package) to their flag values.
AttrNameValueMap map[string]uint32 = map[string]uint32{
"SecureDelete": SecureDelete,
"UnDelete": UnDelete,
"CompressFile": CompressFile,
"SyncUpdatechattr": SyncUpdatechattr,
"Immutable": Immutable,
"AppendOnly": AppendOnly,
"NoDumpFile": NoDumpFile,
"NoUpdateAtime": NoUpdateAtime,
"IsDirty": IsDirty,
"CompressedClusters": CompressedClusters,
"NoCompress": NoCompress,
"EncFile": EncFile,
"BtreeFmt": BtreeFmt,
"HashIdxDir": HashIdxDir,
"AfsDir": AfsDir,
"ReservedExt3": ReservedExt3,
"NoMergeTail": NoMergeTail,
"DirSync": DirSync,
"DirTop": DirTop,
"ReservedExt4a": ReservedExt4a,
"Extents": Extents,
"LargeEaInode": LargeEaInode,
"ReservedExt4b": ReservedExt4b,
"NoCOWFile": NoCOWFile,
"ReservedExt4c": ReservedExt4c,
"UseParentProjId": UseParentProjId,
"ReservedExt2": ReservedExt2,
}
/* /*
AttrValueNameMap contains a mapping of attribute flags to their names (as designated by this package). linuxFsAttrsListOrder defines the order the attributes are printed in per e2fsprogs.
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so be forewarned.
See flags_name at https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/e2p/pf.c for order.
Up to date as of e2fsprogs v1.47.1, Linux 6.12-rc7.

The below are the struct field names for easier reflection.
*/ */
AttrValueNameMap map[uint32]string = map[uint32]string{ linuxFsAttrsListOrder []string = []string{
SecureDelete: "SecureDelete", "SecureDelete",
UnDelete: "UnDelete", "UnDelete",
CompressFile: "CompressFile", "SyncUpdate",
SyncUpdatechattr: "SyncUpdatechattr", "DirSync",
Immutable: "Immutable", "Immutable",
AppendOnly: "AppendOnly", "AppendOnly",
NoDumpFile: "NoDumpFile", "NoDumpFile",
NoUpdateAtime: "NoUpdateAtime", "NoUpdateAtime",
IsDirty: "IsDirty", "CompressFile",
CompressedClusters: "CompressedClusters", "EncFile",
NoCompress: "NoCompress", "ReservedExt3",
EncFile: "EncFile", "HashIdxDir",
BtreeFmt: "BtreeFmt|HashIdxDir", // Well THIS is silly and seems like an oversight. Both FS_BTREE_FL and FS_INDEX_FL have the same flag. Confirmed in kernel source. "NoMergeTail",
AfsDir: "AfsDir", "DirTop",
ReservedExt3: "ReservedExt3", "Extents",
NoMergeTail: "NoMergeTail", "NoCOWFile",
DirSync: "DirSync", "DAX",
DirTop: "DirTop", "CaseInsensitive",
ReservedExt4a: "ReservedExt4a", "ReservedExt4c",
Extents: "Extents", "UseParentProjId",
LargeEaInode: "LargeEaInode", "VerityProtected",
ReservedExt4b: "ReservedExt4b", "NoCompress",
NoCOWFile: "NoCOWFile",
ReservedExt4c: "ReservedExt4c",
UseParentProjId: "UseParentProjId",
ReservedExt2: "ReservedExt2",
} }
) )

127
fsutils/consts_lin.go Normal file
View File

@ -0,0 +1,127 @@
package fsutils

/*
https://github.com/torvalds/linux/blob/master/include/uapi/linux/fs.h "Inode flags (FS_IOC_GETFLAGS / FS_IOC_SETFLAGS)"
Up to date as of Linux 6.12-rc7.
*/
const (
SecureDelete fsAttr = 1 << iota // Secure deletion
UnDelete // Undelete
CompressFile // Compress file
SyncUpdate // Synchronous updates
Immutable // Immutable file
AppendOnly // Writes to file may only append
NoDumpFile // Do not dump file
NoUpdateAtime // Do not update atime
IsDirty // Nobody knows what this does, lol.
CompressedClusters // One or more compressed clusters
NoCompress // Don't compress
EncFile // Encrypted file
BtreeFmt // Btree format dir
AfsDir // AFS directory
ReservedExt3 // Reserved for ext3
NoMergeTail // File tail should not be merged
DirSync // dirsync behaviour (directories only)
DirTop // Top of directory hierarchies
ReservedExt4a // Reserved for ext4
Extents // Extents
VerityProtected // Verity-protected inode
LargeEaInode // Inode used for large EA
ReservedExt4b // Reserved for ext4
NoCOWFile // Do not cow file
_ // (Unused)
DAX // Inode is DAX
_ // (Unused)
_ // (Unused)
ReservedExt4c // Reserved for ext4
UseParentProjId // Create with parents projid
CaseInsensitive // Folder is case-insensitive
ReservedExt2 // Reserved for ext2 lib
)

// These are the same value. For some reason.
const (
HashIdxDir fsAttr = BtreeFmt // Hash-indexed directory
)

var (
// AttrNameValueMap contains a mapping of attribute names (as designated by this package) to their flag values.
AttrNameValueMap map[string]fsAttr = map[string]fsAttr{
"SecureDelete": SecureDelete,
"UnDelete": UnDelete,
"CompressFile": CompressFile,
"SyncUpdate": SyncUpdate,
"Immutable": Immutable,
"AppendOnly": AppendOnly,
"NoDumpFile": NoDumpFile,
"NoUpdateAtime": NoUpdateAtime,
"IsDirty": IsDirty,
"CompressedClusters": CompressedClusters,
"NoCompress": NoCompress,
"EncFile": EncFile,
"BtreeFmt": BtreeFmt,
"HashIdxDir": HashIdxDir,
"AfsDir": AfsDir,
"ReservedExt3": ReservedExt3,
"NoMergeTail": NoMergeTail,
"DirSync": DirSync,
"DirTop": DirTop,
"ReservedExt4a": ReservedExt4a,
"Extents": Extents,
"VerityProtected": VerityProtected,
"LargeEaInode": LargeEaInode,
"ReservedExt4b": ReservedExt4b,
"NoCOWFile": NoCOWFile,
"DAX": DAX,
"ReservedExt4c": ReservedExt4c,
"UseParentProjId": UseParentProjId,
"CaseInsensitive": CaseInsensitive,
"ReservedExt2": ReservedExt2,
}

/*
AttrValueNameMap contains a mapping of attribute flags to their names (as designated by this package).
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so their string value is unpredictable.
*/
AttrValueNameMap map[fsAttr]string = invertMap(AttrNameValueMap)

// KernelNameValueMap allows lookups using the symbol name as used in the Linux kernel source.
KernelNameValueMap map[string]fsAttr = map[string]fsAttr{
"FS_SECRM_FL": SecureDelete,
"FS_UNRM_FL": UnDelete,
"FS_COMPR_FL": CompressFile,
"FS_SYNC_FL": SyncUpdate,
"FS_IMMUTABLE_FL": Immutable,
"FS_APPEND_FL": AppendOnly,
"FS_NODUMP_FL": NoDumpFile,
"FS_NOATIME_FL": NoUpdateAtime,
"FS_DIRTY_FL": IsDirty,
"FS_COMPRBLK_FL": CompressedClusters,
"FS_NOCOMP_FL": NoCompress,
"FS_ENCRYPT_FL": EncFile,
"FS_BTREE_FL": BtreeFmt,
"FS_INDEX_FL": HashIdxDir,
"FS_IMAGIC_FL": AfsDir,
"FS_JOURNAL_DATA_FL": ReservedExt3,
"FS_NOTAIL_FL": NoMergeTail,
"FS_DIRSYNC_FL": DirSync,
"FS_TOPDIR_FL": DirTop,
"FS_HUGE_FILE_FL": ReservedExt4a,
"FS_EXTENT_FL": Extents,
"FS_VERITY_FL": VerityProtected,
"FS_EA_INODE_FL": LargeEaInode,
"FS_EOFBLOCKS_FL": ReservedExt4b,
"FS_NOCOW_FL": NoCOWFile,
"FS_DAX_FL": DAX,
"FS_INLINE_DATA_FL": ReservedExt4c,
"FS_PROJINHERIT_FL": UseParentProjId,
"FS_CASEFOLD_FL": CaseInsensitive,
"FS_RESERVED_FL": ReservedExt2,
}

/*
KernelValueNameMap contains a mapping of attribute flags to their kernel source symbol name.
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so their string value is unpredictable.
*/
KernelValueNameMap map[fsAttr]string = invertMap(KernelNameValueMap)
)

7
fsutils/doc.go Normal file
View File

@ -0,0 +1,7 @@
/*
fsutils is a collection of filesystem-related functions, types, etc.

Currently it's only a (fixed/actually working) reimplementation of github.com/g0rbe/go-chattr.
(Note to library maintainers, if someone reports an integer overflow and even tells you how to fix it, you should probably fix it.)
*/
package fsutils

11
fsutils/errs.go Normal file
View File

@ -0,0 +1,11 @@
package fsutils

import (
`syscall`
)

var (
// Yes, I know. "Why ENOTTY?" I don't know, ask Linus.
// If you see "inappropriate ioctl for device", it's this'un.
ErrFsAttrsUnsupported error = syscall.ENOTTY
)

View File

@ -1,44 +1,16 @@
package fsutils package fsutils


import ( // invertMap returns some handy consts remapping for easier lookups.
`os` func invertMap(origMap map[string]fsAttr) (newMap map[fsAttr]string) {
`reflect`


`github.com/g0rbe/go-chattr` if origMap == nil {
`r00t2.io/sysutils/paths`
)

func GetAttrs(path string) (attrs *FsAttrs, err error) {

var f *os.File
var evalAttrs FsAttrs
var attrVal uint32
var reflectVal reflect.Value
var field reflect.Value
var myPath string = path

if err = paths.RealPath(&myPath); err != nil {
return return
} }
newMap = make(map[fsAttr]string)


if f, err = os.Open(myPath); err != nil { for k, v := range origMap {
return newMap[v] = k
} }
defer f.Close()

reflectVal = reflect.ValueOf(&evalAttrs).Elem()

if attrVal, err = chattr.GetAttrs(f); err != nil {
return
}

for attrNm, attrInt := range AttrNameValueMap {
field = reflectVal.FieldByName(attrNm)
field.SetBool((attrVal & attrInt) != 0)
}

attrs = new(FsAttrs)
*attrs = evalAttrs


return return
} }

View File

@ -1,43 +1,96 @@
package fsutils package fsutils


import ( import (
`os`
`reflect` `reflect`

`strings`
`github.com/g0rbe/go-chattr`
`r00t2.io/sysutils/paths`
) )


func (f *FsAttrs) Apply(path string) (err error) { /*
String returns a string representation (comparable to lsattr(1)) of an FsAttrs.


var file *os.File Not all flags are represented, as this aims for compatibility with e2fsprogs/lsattr output.
var reflectVal reflect.Value */
func (f *FsAttrs) String() (s string) {

// Flags have their short name printed if set, otherwise a '-' placeholder is used.
// https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/e2p/pf.c

var refType reflect.Type
var refVal reflect.Value
var refField reflect.StructField
var fieldVal reflect.Value var fieldVal reflect.Value
var tagVal string
var sb strings.Builder


var myPath string = path if f == nil {

s = strings.Repeat("-", len(linuxFsAttrsListOrder))
if err = paths.RealPath(&myPath); err != nil {
return return
} }
if file, err = os.Open(myPath); err != nil {
return
}
defer file.Close()


reflectVal = reflect.ValueOf(*f) refVal = reflect.ValueOf(*f)

refType = refVal.Type()
for attrNm, attrVal := range AttrNameValueMap { for _, fn := range linuxFsAttrsListOrder {
fieldVal = reflectVal.FieldByName(attrNm) refField, _ = refType.FieldByName(fn)
tagVal = refField.Tag.Get("fsAttrShort")
if tagVal == "" || tagVal == "-" {
continue
}
fieldVal = refVal.FieldByName(fn)
if fieldVal.Bool() { if fieldVal.Bool() {
if err = chattr.SetAttr(file, attrVal); err != nil { sb.WriteString(tagVal)
return
}
} else { } else {
if err = chattr.UnsetAttr(file, attrVal); err != nil { sb.WriteString("-")
return
}
} }
} }


s = sb.String()

return
}

/*
StringLong returns a more extensive/"human-friendly" representation (comparable to lsattr(1) wiih -l) of an Fsattrs.

Not all flags are represented, as this aims for compatibility with e2fsprogs/lsattr output.
*/
func (f *FsAttrs) StringLong() (s string) {

// The long names are separated via a commma then a space.
// If no attrs are set, the string "---" is used.
// https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git/tree/lib/e2p/pf.c

var refType reflect.Type
var refVal reflect.Value
var refField reflect.StructField
var fieldVal reflect.Value
var tagVal string
var out []string

if f == nil {
s = strings.Repeat("-", 3)
return
}

refVal = reflect.ValueOf(*f)
refType = refVal.Type()
for _, fn := range linuxFsAttrsListOrder {
refField, _ = refType.FieldByName(fn)
tagVal = refField.Tag.Get("fsAttrLong")
if tagVal == "" || tagVal == "-" {
continue
}
fieldVal = refVal.FieldByName(fn)
if fieldVal.Bool() {
out = append(out, tagVal)
}
}

if out == nil || len(out) == 0 {
s = strings.Repeat("-", 3)
return
}

s = strings.Join(out, ", ")

return return
} }

View File

@ -0,0 +1,46 @@
//go:build linux

package fsutils

import (
`os`
`reflect`

`r00t2.io/sysutils/paths`
)

func (f *FsAttrs) Apply(path string) (err error) {

var file *os.File
var reflectVal reflect.Value
var fieldVal reflect.Value

if f == nil {
return
}

if err = paths.RealPath(&path); err != nil {
return
}
if file, err = os.Open(path); err != nil {
return
}
defer file.Close()

reflectVal = reflect.ValueOf(*f)

for attrNm, attrVal := range AttrNameValueMap {
fieldVal = reflectVal.FieldByName(attrNm)
if fieldVal.Bool() {
if err = setAttrs(file, attrVal); err != nil {
return
}
} else {
if err = unsetAttrs(file, attrVal); err != nil {
return
}
}
}

return
}

134
fsutils/funcs_linux.go Normal file
View File

@ -0,0 +1,134 @@
//go:build linux

package fsutils

import (
`os`
`reflect`
`unsafe`

`golang.org/x/sys/unix`
`r00t2.io/goutils/bitmask`
`r00t2.io/sysutils/paths`
)

func GetAttrs(path string) (attrs *FsAttrs, err error) {

var f *os.File
var evalAttrs FsAttrs
var attrVal fsAttr
var attrValBit bitmask.MaskBit
var reflectVal reflect.Value
var field reflect.Value
var myPath string = path

if err = paths.RealPath(&myPath); err != nil {
return
}

if f, err = os.Open(myPath); err != nil {
return
}
defer f.Close()

reflectVal = reflect.ValueOf(&evalAttrs).Elem()

if attrVal, err = getAttrs(f); err != nil {
return
}
attrValBit = bitmask.MaskBit(attrVal)

for attrNm, attrInt := range AttrNameValueMap {
field = reflectVal.FieldByName(attrNm)
field.SetBool(attrValBit.HasFlag(bitmask.MaskBit(attrInt)))
}

attrs = new(FsAttrs)
*attrs = evalAttrs

return
}

// getAttrs is the unexported low-level syscall to get attributes.
func getAttrs(f *os.File) (attrVal fsAttr, err error) {

var u uint
var curFlags int
// var errNo syscall.Errno

/*
if _, _, errNo = unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.FS_IOC_GETFLAGS, uintptr(unsafe.Pointer(&curFlags))); errNo != 0 {
err = os.NewSyscallError("ioctl: FS_IOC_GETFLAGS", errNo)
return
}
*/
if curFlags, err = unix.IoctlGetInt(int(f.Fd()), unix.FS_IOC_GETFLAGS); err != nil {
return
}

u = uint(curFlags)

attrVal = fsAttr(u)

return
}

// setAttrs is the unexported low-level syscall to set attributes. attrs may be OR'd.
func setAttrs(f *os.File, attrs fsAttr) (err error) {

var curAttrs fsAttr
var ab bitmask.MaskBit
var errNo unix.Errno
var val uint

if curAttrs, err = getAttrs(f); err != nil {
return
}
ab = bitmask.MaskBit(curAttrs)

if ab.HasFlag(bitmask.MaskBit(attrs)) {
return
}

ab.AddFlag(bitmask.MaskBit(attrs))

val = ab.Value()

/*
if err = unix.IoctlSetInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, int(ab.Value())); err != nil {
return
}
*/
if _, _, errNo = unix.Syscall(unix.SYS_IOCTL, f.Fd(), unix.FS_IOC_SETFLAGS, uintptr(unsafe.Pointer(&val))); errNo != 0 {
err = os.NewSyscallError("ioctl: SYS_IOCTL", errNo)
return
}

return
}

// unsetAttrs is the unexported low-level syscall to remove attributes. attrs may be OR'd.
func unsetAttrs(f *os.File, attrs fsAttr) (err error) {

var curAttrs fsAttr
var ab bitmask.MaskBit

if curAttrs, err = getAttrs(f); err != nil {
return
}
ab = bitmask.MaskBit(curAttrs)

if !ab.HasFlag(bitmask.MaskBit(attrs)) {
return
}

ab.ClearFlag(bitmask.MaskBit(attrs))

/*
if err = unix.IoctlSetInt(int(f.Fd()), unix.FS_IOC_SETFLAGS, int(ab.Value())); err != nil {
return
}
*/

return
}

View File

@ -1,3 +1,5 @@
//go:build linux

package fsutils package fsutils


import ( import (
@ -7,12 +9,13 @@ import (
`os/user` `os/user`
`testing` `testing`


`github.com/davecgh/go-spew/spew`
`r00t2.io/sysutils/paths` `r00t2.io/sysutils/paths`
) )


var ( var (
testFilename string = "testfile" testFilename string = "testfile"
testErrBadUser error = errors.New("test must be run as root, on Linux") testErrBadUser error = errors.New("test must be run as root")
) )


func testChkUser() (err error) { func testChkUser() (err error) {
@ -36,12 +39,18 @@ func TestSetAttrs(t *testing.T) {
if attrs, err = GetAttrs(testFilename); err != nil { if attrs, err = GetAttrs(testFilename); err != nil {
t.Fatalf("Failed to get attrs for %v: %v", testFilename, err) t.Fatalf("Failed to get attrs for %v: %v", testFilename, err)
} }
t.Logf("Attrs for %v:\n%#v", testFilename, attrs) t.Logf("Attrs for %v (before):\n%s", testFilename, spew.Sdump(attrs))
attrs.CompressFile = true attrs.CompressFile = true
attrs.SyncUpdate = true
attrs.SecureDelete = true
if err = attrs.Apply(testFilename); err != nil { if err = attrs.Apply(testFilename); err != nil {
t.Fatalf("Failed to apply attrs to %v: %v", testFilename, err) t.Fatalf("Failed to apply attrs to %v: %v", testFilename, err)
} }
t.Logf("Applied new attrs to %v:\n%#v", testFilename, attrs) t.Logf("Applied new attrs to %v:\n%#v", testFilename, attrs)
if attrs, err = GetAttrs(testFilename); err != nil {
t.Fatalf("Failed to get attrs for %v: %v", testFilename, err)
}
t.Logf("Attrs for %v (after):\n%s", testFilename, spew.Sdump(attrs))
} }


func TestMain(t *testing.M) { func TestMain(t *testing.M) {

View File

@ -1,32 +1,44 @@
package fsutils package fsutils


// FsAttrs is a convenience struct around github.com/g0rbe/go-chattr. import (
`r00t2.io/goutils/bitmask`
)

type fsAttr bitmask.MaskBit

/*
FsAttrs is a struct representation of filesystem attributes on Linux.
Up to date as of Linux 6.12-rc7.
*/
type FsAttrs struct { type FsAttrs struct {
SecureDelete bool SecureDelete bool `fsAttrShort:"s" fsAttrLong:"Secure_Deletion" fsAttrKern:"FS_SECRM_FL" json:"secure_delete" toml:"SecureDelete" yaml:"Secure Delete" xml:"secureDelete,attr"`
UnDelete bool UnDelete bool `fsAttrShort:"u" fsAttrLong:"Undelete" fsAttrKern:"FS_UNRM_FL" json:"undelete" toml:"Undelete" yaml:"Undelete" xml:"undelete,attr"`
CompressFile bool CompressFile bool `fsAttrShort:"c" fsAttrLong:"Compression_Requested" fsAttrKern:"FS_COMPR_FL" json:"compress" toml:"Compress" yaml:"Compress" xml:"compress,attr"`
SyncUpdatechattr bool SyncUpdate bool `fsAttrShort:"S" fsAttrLong:"Synchronous_Updates" fsAttrKern:"FS_SYNC_FL" json:"sync" toml:"SyncUpdate" yaml:"Synchronized Update" xml:"syncUpdate,attr"`
Immutable bool Immutable bool `fsAttrShort:"i" fsAttrLong:"Immutable" fsAttrKern:"FS_IMMUTABLE_FL" json:"immutable" toml:"Immutable" yaml:"Immutable" xml:"immutable,attr"`
AppendOnly bool AppendOnly bool `fsAttrShort:"a" fsAttrLong:"Append_Only" fsAttrKern:"FS_APPEND_FL" json:"append_only" toml:"AppendOnly" yaml:"Append Only" xml:"appendOnly,attr"`
NoDumpFile bool NoDumpFile bool `fsAttrShort:"d" fsAttrLong:"No_Dump" fsAttrKern:"FS_NODUMP_FL" json:"no_dump" toml:"NoDump" yaml:"Disable Dumping" xml:"noDump,attr"`
NoUpdateAtime bool NoUpdateAtime bool `fsAttrShort:"A" fsAttrLong:"No_Atime" fsAttrKern:"FS_NOATIME_FL" json:"no_atime" toml:"DisableAtime" yaml:"Disable Atime Updating" xml:"noAtime,attr"`
IsDirty bool IsDirty bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_DIRTY_FL" json:"dirty" toml:"Dirty" yaml:"Dirty" xml:"dirty,attr"`
CompressedClusters bool CompressedClusters bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_COMPRBLK_FL" json:"compress_clst" toml:"CompressedClusters" yaml:"Compressed Clusters" xml:"compressClst,attr"`
NoCompress bool NoCompress bool `fsAttrShort:"m" fsAttrLong:"Dont_Compress" fsAttrKern:"FS_NOCOMP_FL" json:"no_compress" toml:"DisableCompression" yaml:"Disable Compression" xml:"noCompress,attr"`
EncFile bool EncFile bool `fsAttrShort:"E" fsAttrLong:"Encrypted" fsAttrKern:"FS_ENCRYPT_FL" json:"enc" toml:"Encrypted" yaml:"Encrypted" xml:"enc,attr"`
BtreeFmt bool BtreeFmt bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_BTREE_FL" json:"btree" toml:"Btree" yaml:"Btree" xml:"btree,attr"`
HashIdxDir bool HashIdxDir bool `fsAttrShort:"I" fsAttrLong:"Indexed_directory" fsAttrKern:"FS_INDEX_FL" json:"idx_dir" toml:"IdxDir" yaml:"Indexed Directory" xml:"idxDir,attr"`
AfsDir bool AfsDir bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_IMAGIC_FL" json:"afs" toml:"AFS" yaml:"AFS" xml:"afs,attr"`
ReservedExt3 bool ReservedExt3 bool `fsAttrShort:"j" fsAttrLong:"Journaled_Data" fsAttrKern:"FS_JOURNAL_DATA_FL" json:"res_ext3" toml:"ReservedExt3" yaml:"Reserved Ext3" xml:"resExt3,attr"`
NoMergeTail bool NoMergeTail bool `fsAttrShort:"t" fsAttrLong:"No_Tailmerging" fsAttrKern:"FS_NOTAIL_FL" json:"no_merge_tail" toml:"DisableTailmerging" yaml:"Disable Tailmerging" xml:"noMergeTail,attr"`
DirSync bool DirSync bool `fsAttrShort:"D" fsAttrLong:"Synchronous_Directory_Updates" fsAttrKern:"FS_DIRSYNC_FL" json:"dir_sync" toml:"DirSync" yaml:"Synchronized Directory Updates" xml:"dirSync,attr"`
DirTop bool DirTop bool `fsAttrShort:"T" fsAttrLong:"Top_of_Directory_Hierarchies" fsAttrKern:"FS_TOPDIR_FL" json:"dir_top" toml:"DirTop" yaml:"Top of Directory Hierarchies" xml:"dirTop,attr"`
ReservedExt4a bool ReservedExt4a bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_HUGE_FILE_FL" json:"res_ext4a" toml:"ReservedExt4A" yaml:"Reserved Ext4 A" xml:"resExt4a,attr"`
Extents bool Extents bool `fsAttrShort:"e" fsAttrLong:"Extents" fsAttrKern:"FS_EXTENT_FL" json:"extents" toml:"Extents" yaml:"Extents" xml:"extents,attr"`
LargeEaInode bool VerityProtected bool `fsAttrShort:"V" fsAttrLong:"Verity" fsAttrKern:"FS_VERITY_FL" json:"verity" toml:"Verity" yaml:"Verity Protected" xml:"verity,attr"`
ReservedExt4b bool LargeEaInode bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_EA_INODE_FL" json:"ea" toml:"EAInode" yaml:"EA Inode" xml:"ea,attr"`
NoCOWFile bool ReservedExt4b bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_EOFBLOCKS_FL" json:"res_ext4b" toml:"ReservedExt4B" yaml:"Reserved Ext4 B" xml:"resExt4b,attr"`
ReservedExt4c bool NoCOWFile bool `fsAttrShort:"C" fsAttrLong:"No_COW" fsAttrKern:"FS_NOCOW_FL" json:"no_cow" toml:"NoCOW" yaml:"Disable COW" xml:"noCOW,attr"`
UseParentProjId bool DAX bool `fsAttrShort:"x" fsAttrLong:"DAX" fsAttrKern:"FS_DAX_FL" json:"dax" toml:"DAX" yaml:"DAX" xml:"DAX,attr"`
ReservedExt2 bool ReservedExt4c bool `fsAttrShort:"N" fsAttrLong:"Inline_Data" fsAttrKern:"FS_INLINE_DATA_FL" json:"res_ext4c" toml:"ReservedExt4C" yaml:"Reserved Ext4 C" xml:"resExt4c,attr"`
UseParentProjId bool `fsAttrShort:"P" fsAttrLong:"Project_Hierarchy" fsAttrKern:"FS_PROJINHERIT_FL" json:"parent_proj_id" toml:"ParentProjId" yaml:"Use Parent Project ID" xml:"parentProjId,attr"`
CaseInsensitive bool `fsAttrShort:"F" fsAttrLong:"Casefold" fsAttrKern:"FS_CASEFOLD_FL" json:"case_ins" toml:"CaseInsensitive" yaml:"Case Insensitive" xml:"caseIns,attr"`
ReservedExt2 bool `fsAttrShort:"-" fsAttrLong:"-" fsAttrKern:"FS_RESERVED_FL" json:"res_ext2" toml:"ReservedExt2" yaml:"Reserved Ext2" xml:"resExt2,attr"`
} }

8
go.mod
View File

@ -4,12 +4,10 @@ go 1.23.2


require ( require (
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/g0rbe/go-chattr v1.0.1 github.com/djherbis/times v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
golang.org/x/sync v0.9.0
golang.org/x/sys v0.26.0 golang.org/x/sys v0.26.0
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
r00t2.io/goutils v1.7.0 r00t2.io/goutils v1.7.1
) )

// Pending https://github.com/g0rbe/go-chattr/pull/3
replace github.com/g0rbe/go-chattr => github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13

13
go.sum
View File

@ -1,18 +1,19 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 h1:tgEbuE4bNVjaCWWIB1u9lDzGqH/ZdBTg33+4vNW2rjg= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E= honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE= honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
r00t2.io/goutils v1.7.0 h1:iQluWlkOyBwOKaK94D5QSnSMYpGKtMb/5WjefmdfHgI= r00t2.io/goutils v1.7.1 h1:Yzl9rxX1sR9WT0FcjK60qqOgBoFBOGHYKZVtReVLoQc=
r00t2.io/goutils v1.7.0/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= r00t2.io/goutils v1.7.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
r00t2.io/sysutils v1.7.0 h1:zk5IbcbZvq11FoXI/fLvcgyq36lBhPDY6fvC9CunfWE=
r00t2.io/sysutils v1.7.0/go.mod h1:Sk/7riJp9fteeW9STkdQ/k22huL1J6r05n6wLh5byHY=

42
paths/consts.go Normal file
View File

@ -0,0 +1,42 @@
package paths

import (
"io/fs"
)

// Mostly just for reference.
const (
// ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular
modeDir pathMode = pathMode(fs.ModeDir)
modeSymlink pathMode = pathMode(fs.ModeSymlink)
modePipe pathMode = pathMode(fs.ModeNamedPipe)
modeSocket pathMode = pathMode(fs.ModeSocket)
modeDev pathMode = pathMode(fs.ModeDevice)
modeCharDev pathMode = pathMode(fs.ModeCharDevice)
modeIrregular pathMode = pathMode(fs.ModeIrregular)
modeAnyExceptRegular pathMode = modeDir | modeSymlink | modePipe | modeSocket | modeDev | modeCharDev | modeIrregular
)

// Miss reasons
const (
MissNoMiss missReason = ""
MissNoMeta missReason = "Could not determine metadata"
MissBadBase missReason = "Base name does not match BasePtrn"
MissBadPath missReason = "Path does not match PathPtrn"
MissBadTime missReason = "Time(s) does not/do not match Age"
MissFile missReason = "Object is a file and NoFiles is set"
MissType missReason = "Object does not match TargetType"
)

// Times
const TimeAny pathTimeType = 0
const (
// TimeAccessed == atime
TimeAccessed pathTimeType = 1 << iota
// TimeCreated == "birth" time (*NOT* ctime! See TimeChanged)
TimeCreated
// TimeChanged == ctime
TimeChanged
// TimeModified == mtime
TimeModified
)

12
paths/errs.go Normal file
View File

@ -0,0 +1,12 @@
package paths

import (
`errors`
)

var (
ErrNilErrChan error = errors.New("an initialized error channel is required")
ErrNilMatchChan error = errors.New("an initialized matches channel is required")
ErrNilMismatchChan error = errors.New("an initialized mismatches channel is required")
ErrNilWg error = errors.New("a non-nil sync.WaitGroup is required")
)

View File

@ -19,14 +19,22 @@
package paths package paths


import ( import (
`context`
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
`sort`
"strings" "strings"
`sync`
`time`

// "syscall" // "syscall"

`github.com/djherbis/times`
`r00t2.io/goutils/bitmask`
) )


/* /*
@ -266,3 +274,236 @@ func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error)


return return
} }

// SearchFsPaths gets a file/directory/etc. path list based on the provided criteria.
func SearchFsPaths(matcher FsSearchCriteria) (found, miss []*FsSearchResult, err error) {

var matched *FsSearchResult
var missed *FsSearchResult

if err = RealPath(&matcher.Root); err != nil {
return
}

if err = filepath.WalkDir(
matcher.Root,
func(path string, d fs.DirEntry, inErr error) (outErr error) {

if inErr != nil {
outErr = inErr
return
}

if matched, missed, outErr = matcher.Match(path, d, nil); outErr != nil {
return
}
if matched != nil && !matcher.NoMatch {
found = append(found, matched)
}
if missed != nil && !matcher.NoMismatch {
miss = append(miss, missed)
}

return
},
); err != nil {
return
}

if found == nil || len(found) == 0 {
return
}

// And sort them.
sort.Slice(
found,
func(i, j int) (isLess bool) {
isLess = found[i].Path < found[j].Path
return
},
)

return
}

/*
SearchFsPathsAsync is exactly like SearchFsPaths, but dispatches off concurrent
workers for the filtering logic instead of performing iteratively/recursively.
It may, in some cases, be *slightly more* performant and *slightly less* in others.
Note that unlike SearchFsPaths, the results written to the
FsSearchCriteriaAsync.ResChan are not guaranteed to be in any predictable order.

All channels are expected to have already been initialized by the caller.
They will not be closed by this function.
*/
func SearchFsPathsAsync(matcher FsSearchCriteriaAsync) {

var err error
var wgLocal sync.WaitGroup
var doneChan chan bool = make(chan bool, 1)

if matcher.ErrChan == nil {
panic(ErrNilErrChan)
return
}

if matcher.WG == nil {
matcher.ErrChan <- ErrNilWg
return
}

defer matcher.WG.Done()

if matcher.ResChan == nil && !matcher.NoMatch {
matcher.ErrChan <- ErrNilMatchChan
return
}
if matcher.MismatchChan == nil && !matcher.NoMismatch {
matcher.ErrChan <- ErrNilMismatchChan
return
}

if err = RealPath(&matcher.Root); err != nil {
matcher.ErrChan <- err
return
}

if matcher.Semaphore != nil && matcher.SemaphoreCtx == nil {
matcher.SemaphoreCtx = context.Background()
}

if err = filepath.WalkDir(
matcher.Root,
func(path string, de fs.DirEntry, inErr error) (outErr error) {

if inErr != nil {
inErr = filterNoFileDir(inErr)
if inErr != nil {
outErr = inErr
return
}
}

wgLocal.Add(1)
if matcher.Semaphore != nil {
if err = matcher.Semaphore.Acquire(matcher.SemaphoreCtx, 1); err != nil {
return
}
}

go func(p string, d fs.DirEntry) {
var pErr error
var pResMatch *FsSearchResult
var pResMiss *FsSearchResult

defer wgLocal.Done()

if matcher.Semaphore != nil {
defer matcher.Semaphore.Release(1)
}

if pResMatch, pResMiss, pErr = matcher.Match(p, d, nil); pErr != nil {
matcher.ErrChan <- pErr
return
}

if pResMatch != nil && !matcher.NoMatch {
matcher.ResChan <- pResMatch
}
if pResMiss != nil && !matcher.NoMismatch {
matcher.MismatchChan <- pResMiss
}
}(path, de)

return
},
); err != nil {
err = filterNoFileDir(err)
if err != nil {
matcher.ErrChan <- err
return
}
}

go func() {
wgLocal.Wait()
doneChan <- true
}()

<-doneChan

return
}

/*
filterTimes checks a times.Timespec of a file using:
* an age specified by the caller
* an ageType bitmask for types of times to compare
* an olderThan bool (if false, the file must be younger than)
* an optional "now" timestamp for the age derivation.
*/
func filterTimes(tspec times.Timespec, age *time.Duration, ageType *pathTimeType, olderThan bool, now *time.Time) (include bool) {

var curAge time.Duration
var mask *bitmask.MaskBit
var tfunc func(t *time.Duration) (match bool) = func(t *time.Duration) (match bool) {
if olderThan {
match = *t > *age
} else {
match = *t < *age
}
return
}

if tspec == nil || age == nil || ageType == nil {
return
}

mask = ageType.Mask()

if now == nil {
now = new(time.Time)
*now = time.Now()
}

// BTIME (if supported)
if tspec.HasBirthTime() && (mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeCreated))) {
curAge = now.Sub(tspec.BirthTime())
if include = tfunc(&curAge); include {
return
}
}
// MTIME
if mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeModified)) {
curAge = now.Sub(tspec.ModTime())
if include = tfunc(&curAge); include {
return
}
}
// CTIME (if supported)
if tspec.HasChangeTime() && (mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeChanged))) {
curAge = now.Sub(tspec.ChangeTime())
if include = tfunc(&curAge); include {
return
}
}
// ATIME
if mask.HasFlag(bitmask.MaskBit(TimeAny)) || mask.HasFlag(bitmask.MaskBit(TimeAccessed)) {
curAge = now.Sub(tspec.AccessTime())
if include = tfunc(&curAge); include {
return
}
}

return
}

func filterNoFileDir(err error) (filtered error) {

filtered = err
if errors.Is(err, fs.ErrNotExist) {
filtered = nil
}

return
}

View File

@ -0,0 +1,125 @@
package paths

import (
`io/fs`
`os`
`path/filepath`
`time`

`github.com/djherbis/times`
`r00t2.io/goutils/bitmask`
)

/*
Match returns match (a ptr to a FsSearchResult if the specified path matches, otherwise nil),
miss (ptr the specified path does not match, otherwise nil), and an fs.DirEntry and fs.FileInfo
for path. d and/or fi may be nil.

If err is not nil, it represents an unexpected error and as such, both match and miss should be nil.

Match, miss, and err will all be nil if the filesystem object/path does not exist.
*/
func (f *FsSearchCriteria) Match(path string, d fs.DirEntry, fi fs.FileInfo) (match, miss *FsSearchResult, err error) {

var typeMode fs.FileMode
var m FsSearchResult
var typeFilter *bitmask.MaskBit = bitmask.NewMaskBitExplicit(uint(f.TargetType))

m = FsSearchResult{
Path: path,
DirEntry: d,
FileInfo: fi,
Criteria: f,
}

if f == nil {
return
}

// A DirEntry can be created from a FileInfo but not vice versa.
if m.FileInfo == nil {
if m.DirEntry != nil {
if m.FileInfo, err = m.DirEntry.Info(); err != nil {
err = filterNoFileDir(err)
if err != nil {
return
}
}
} else {
if f.FollowSymlinks {
if m.FileInfo, err = os.Stat(path); err != nil {
err = filterNoFileDir(err)
if err != nil {
return
}
}
} else {
if m.FileInfo, err = os.Lstat(path); err != nil {
err = filterNoFileDir(err)
if err != nil {
return
}
}
}
m.DirEntry = fs.FileInfoToDirEntry(m.FileInfo)
}
}
if m.DirEntry == nil {
m.DirEntry = fs.FileInfoToDirEntry(m.FileInfo)
}
if m.DirEntry == nil || m.FileInfo == nil {
m.MissReason = MissNoMeta
miss = &m
return
}

if m.Times, err = times.Stat(path); err != nil {
err = filterNoFileDir(err)
if err != nil {
return
}
}

if f.PathPtrn != nil && !f.PathPtrn.MatchString(path) {
m.MissReason = MissBadPath
miss = &m
return
}
if f.BasePtrn != nil && !f.BasePtrn.MatchString(filepath.Base(path)) {
m.MissReason = MissBadBase
miss = &m
return
}

// age
if f.Age != nil {
if f.Now == nil {
f.Now = new(time.Time)
*f.Now = time.Now()
}
if !filterTimes(m.Times, f.Age, &f.AgeType, f.OlderThan, f.Now) {
m.MissReason = MissBadTime
miss = &m
return
}
}

// fs object type (file, dir, etc.)
typeMode = m.FileInfo.Mode().Type()
if typeMode == 0 && f.NoFiles {
m.MissReason = MissFile
miss = &m
return
} else if typeMode != 0 {
if !typeFilter.HasFlag(bitmask.MaskBit(typeMode)) {
m.MissReason = MissType
miss = &m
return
}
}

// If it gets to here, it matches.
match = &m

return
}

View File

@ -0,0 +1,13 @@
package paths

import (
`r00t2.io/goutils/bitmask`
)

// Mask returns a bitmask.MaskBit from a pathTimeType.
func (p *pathTimeType) Mask() (mask *bitmask.MaskBit) {

mask = bitmask.NewMaskBitExplicit(uint(*p))

return
}

136
paths/types.go Normal file
View File

@ -0,0 +1,136 @@
package paths

import (
`context`
`io/fs`
`regexp`
`sync`
`time`

`github.com/djherbis/times`
`golang.org/x/sync/semaphore`
`r00t2.io/goutils/bitmask`
)

// FsSearchCriteria contains filter criteria for SearchFsPaths* functions.
type FsSearchCriteria struct {
// Root indicates the root to search.
Root string `json:"root" toml:"RootPath" yaml:"Root Path" xml:"root,attr" validate:"dir"`
// NoMatch, if true, will not return matches. If NoMatch and NoMismatch are both true, no results will be returned.
NoMatch bool `json:"no_match" toml:"NoMatch" yaml:"No Matches" xml:"noMatch,attr"`
// NoMismatch, if true, will not return mismatches. If NoMatch and NoMismatch are both true, no results will be returned.
NoMismatch bool `json:"no_miss" toml:"NoMismatch" yaml:"No Mismatches" xml:"noMiss,attr"`
/*
TargetType defines what types of filesystem objects should be matched.
It can consist of one or more (io/)fs.FileMode types OR'd together
(ensure they are part of (io/)fs.ModeType).
(You can use 0 to match regular files explicitly, and/or NoFiles = true to exclude them.)
*/
TargetType fs.FileMode `json:"type_tgt" toml:"TargetType" yaml:"Target Type" xml:"typeTgt,attr"`
// NoFiles excludes files from TargetType-matching (as there isn't a way to explicitly exclude files otherwise if a non-zero mode is given).
NoFiles bool `json:"no_file" toml:"ExcludeFiles" yaml:"Exclude Files" xml:"noFile,attr"`
// FollowSymlinks, if true and a path being tested is a symlink, will use metadata (age, etc.) of the symlink itself rather than the link target.
FollowSymlinks bool `json:"follow_sym" toml:"FollowSymlinks" yaml:"Follow Symlinks" xml:"followSym,attr"`
// BasePtrn, if specified, will apply to the *base name (that is, quux.txt rather than /foo/bar/baz/quux.txt). See also PathPtrn.
BasePtrn *regexp.Regexp `json:"ptrn_base,omitempty" toml:"BaseNamePattern,omitempty" yaml:"Base Name Pattern,omitempty" xml:"ptrnBase,attr,omitempty"`
// PathPtrn, if specified, will apply to the *full path* (e.g. /foo/bar/baz/quux.txt, not just quux.txt). See also BasePtrn.
PathPtrn *regexp.Regexp `json:"ptrn_path,omitempty" toml:"PathPattern,omitempty" yaml:"Path Pattern,omitempty" xml:"ptrnPath,attr,omitempty"`
/*
Age, if specified, indicates the comparison of Now againt the AgeType of filesystem objects.
Use OlderThan to indicate if it should be older or newer.
*/
Age *time.Duration `json:"age,omitempty" toml:"Age,omitempty" yaml:"Age,omitempty" xml:"age,attr,omitempty"`
/*
AgeType can be one (or more, OR'd together) of the Time* constants in this package (TimeAny, TimeAccessed, TimeCreated,
TimeChanged, TimeModified) to indicate what timestamp(s) to use for comparing Age.

The zero-value is TimeAny.

The first matching timestamp will pass all time comparisons.
Be mindful of timestamp type support/limitations per OS/filesystem of Root.

Completely unused if Age is nil.
*/
AgeType pathTimeType `json:"type_age" toml:"AgeType" yaml:"Age Type" xml:"typeAge,attr"`
/*
OlderThan, if true (and Age is not nil), indicates that matching filesystem objects should have their
AgeType older than Now. If false, their AgeType should be *newer* than Now.

Completely unused if Age is nil.
*/
OlderThan bool `json:"older" toml:"OlderThan" yaml:"Older Than" xml:"older,attr"`
/*
Now expresses a time to compare to Age via AgeType and OlderThan.
Note that it may be any valid time, not necessarily "now".
If Age is specified but Now is nil, it will be populated with time.Now() when the search is invoked.

Completely unused if Age is nil.
*/
Now *time.Time `json:"now,omitempty" toml:"Now,omitempty" yaml:"Now,omitempty" xml:"now,attr,omitempty"`
}

// FsSearchCriteriaAsync extends FsSearchCriteria for use in an asynchronous (goroutine) manner.
type FsSearchCriteriaAsync struct {
FsSearchCriteria
/*
WG should be a non-nil pointer to a sync.WaitGroup.
This is used to manage searching completion to the caller.

.Done() will be called once within the search function, but no .Add() will be called;
.Add() should be done by the caller beforehand.
*/
WG *sync.WaitGroup
// ResChan must be a non-nil channel for (positive) match results to be sent to.
ResChan chan *FsSearchResult
// MismatchChan, if not nil, will have negative matches/"misses" sent to it.
MismatchChan chan *FsSearchResult
/*
ErrChan should be a non-nil error channel for any unexpected errors encountered.

If nil, a panic will be raised.
*/
ErrChan chan error
/*
Semaphore is completely optional, but if non-nil
it will be used to limit concurrent filesystem
object processing.

It is generally a Very Good Idea(TM) to use this,
as the default is to dispatch all processing concurrently.
This can lead to some heavy I/O and CPU wait.

(See https://pkg.go.dev/golang.org/x/sync/semaphore for details.)
*/
Semaphore *semaphore.Weighted
/*
SemaphoreCtx is the context.Context to use for Semaphore.
If nil (but Sempaphore is not), one will be created locally/internally.
*/
SemaphoreCtx context.Context
}

// FsSearchResult contains a match/miss result for FsSearchCriteria and FsSearchCriteriaAsync.
type FsSearchResult struct {
/*
Path is the path to the object on the filesystem.
It may or may not exist at the time of return,
but will not be an empty string.
*/
Path string `json:"path" toml:"Path" yaml:"Path" xml:"path,attr"`
// DirEntry is the fs.DirEntry for the Path; note that .Name() is the base name only. TODO: serialization?
DirEntry fs.DirEntry `json:"-" toml:"-" yaml:"-" xml:"-"`
// FileInfo is the fs.FileInfo for the Path; note that .Name() is the base name only. TODO: serialization?
FileInfo fs.FileInfo `json:"-" toml:"-" yaml:"-" xml:"-"`
// Criteria is the evaluated criteria specified that this FsSearchResult matched.
Criteria *FsSearchCriteria `json:"criteria" toml:"Criteria" yaml:"Criteria" xml:"criteria"`
// Times holds the mtime, ctime, etc. of the filesystem object (where supported). TODO: serialization?
Times times.Timespec `json:"-" toml:"-" yaml:"-" xml:"-"`
// MissReason contains the reason the result is a miss (MissNoMiss if a match); see the Miss* constants.
MissReason missReason `json:"miss_reason" toml:"MissReason" yaml:"Miss Reason" xml:"miss,attr"`
}

type missReason string

type pathMode bitmask.MaskBit

type pathTimeType bitmask.MaskBit