Compare commits

...

5 Commits

10 changed files with 477 additions and 28 deletions

12
TODO
View File

@ -2,12 +2,24 @@
-- incorporate with https://github.com/tredoe/osutil ?
-- cli flag to dump flat hashes too
--- https://github.com/hlandau/passlib
-- incoprporated separately; https://git.r00t2.io/r00t2/PWGen (import r00t2.io/pwgen)

- unit tests

- constants/vars for errors

- func and struct to return segregated system-level env vars vs. user env vars (mostly usable on windows) (see envs/.TODO.go.UNFINISHED)
-- https://www3.ntu.edu.sg/home/ehchua/programming/howto/Environment_Variables.html
-- windows:
--- https://docs.microsoft.com/en-us/windows/deployment/usmt/usmt-recognized-environment-variables
--- https://pureinfotech.com/list-environment-variables-windows-10/
--- https://gist.github.com/RebeccaWhit3/5dad8627b8227142e1bea432db3f8824
--- https://ss64.com/nt/syntax-variables.html
-- linux/XDG:
--- https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html
--- https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
-- macOS:
--- https://ss64.com/osx/syntax-env_vars.html

- validator for windows usernames, domains, etc. (for *NIX, https://unix.stackexchange.com/a/435120/284004)
-- https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/naming-conventions-for-computer-domain-site-ou

View File

@ -61,6 +61,59 @@ func GetEnvMapNative() (envMap map[string]interface{}) {
return
}

/*
GetFirst gets the first instance if populated/set occurrence of varNames.

For example, if you have three potential env vars, FOO, FOOBAR, FOOBARBAZ,
and want to follow the logic flow of:

1.) Check if FOO is set. If not,
2.) Check if FOOBAR is set. If not,
3.) Check if FOOBARBAZ is set.

Then this would be specified as:

GetFirst([]string{"FOO", "FOOBAR", "FOOBARBAZ"})

If val is "" and ok is true, this means that one of the specified variable names IS
set but is set to an empty value. If ok is false, none of the specified variables
are set.

It is a thin wrapper around GetFirstWithRef.
*/
func GetFirst(varNames []string) (val string, ok bool) {

val, ok, _ = GetFirstWithRef(varNames)

return
}

/*
GetFirstWithRef behaves exactly like GetFirst, but with an additional returned value, idx,
which specifies the index in varNames in which a set variable was found. e.g. if:

GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})

is called and FOO is not set but FOOBAR is, idx will be 1.

If ok is false, idx will always be -1 and should be ignored.
*/
func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {

idx = -1

for i, vn := range varNames {
if HasEnv(vn) {
ok = true
idx = i
val = os.Getenv(vn)
return
}
}

return
}

/*
GetPidEnvMap will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID.
@ -118,3 +171,17 @@ func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {

return
}

/*
HasEnv is much like os.LookupEnv, but only returns a boolean for
if the environment variable key exists or not.

This is useful anywhere you may need to set a boolean in a func call
depending on the *presence* of an env var or not.
*/
func HasEnv(key string) (envIsSet bool) {

_, envIsSet = os.LookupEnv(key)

return
}

101
fsutils/consts.go Normal file
View File

@ -0,0 +1,101 @@
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 (
// 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).
Note the oddball here, BtreeFmt and HashIdxDir are actually the same value, so be forewarned.
*/
AttrValueNameMap map[uint32]string = map[uint32]string{
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", // 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.
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",
}
)

44
fsutils/funcs.go Normal file
View File

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

import (
`os`
`reflect`

`github.com/g0rbe/go-chattr`
`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
}

if f, err = os.Open(myPath); err != nil {
return
}
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
}

43
fsutils/funcs_fsattrs.go Normal file
View File

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

import (
`os`
`reflect`

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

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

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

var myPath string = path

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

reflectVal = reflect.ValueOf(*f)

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

return
}

71
fsutils/funcs_test.go Normal file
View File

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

import (
`errors`
`fmt`
`os`
`os/user`
`testing`

`r00t2.io/sysutils/paths`
)

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

func testChkUser() (err error) {
var u *user.User

if u, err = user.Current(); err != nil {
return
}
if u.Uid != "0" {
err = testErrBadUser
return
}
return
}

func TestSetAttrs(t *testing.T) {

var err error
var attrs *FsAttrs

if attrs, err = GetAttrs(testFilename); err != nil {
t.Fatalf("Failed to get attrs for %v: %v", testFilename, err)
}
t.Logf("Attrs for %v:\n%#v", testFilename, attrs)
attrs.CompressFile = true
if err = attrs.Apply(testFilename); err != nil {
t.Fatalf("Failed to apply attrs to %v: %v", testFilename, err)
}
t.Logf("Applied new attrs to %v:\n%#v", testFilename, attrs)
}

func TestMain(t *testing.M) {

var err error
var rslt int

if err = testChkUser(); err != nil {
fmt.Println(err)
os.Exit(1)
}
if err = paths.RealPath(&testFilename); err != nil {
fmt.Println(err)
os.Exit(2)
}
if err = os.WriteFile(testFilename, []byte("This is a test file."), 0o0644); err != nil {
fmt.Println(err)
os.Exit(3)
}

rslt = t.Run()

if err = os.Remove(testFilename); err != nil {
fmt.Printf("Failed to remove test file %v: %v", testFilename, err)
}
os.Exit(rslt)
}

32
fsutils/types.go Normal file
View File

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

// FsAttrs is a convenience struct around github.com/g0rbe/go-chattr.
type FsAttrs struct {
SecureDelete bool
UnDelete bool
CompressFile bool
SyncUpdatechattr bool
Immutable bool
AppendOnly bool
NoDumpFile bool
NoUpdateAtime bool
IsDirty bool
CompressedClusters bool
NoCompress bool
EncFile bool
BtreeFmt bool
HashIdxDir bool
AfsDir bool
ReservedExt3 bool
NoMergeTail bool
DirSync bool
DirTop bool
ReservedExt4a bool
Extents bool
LargeEaInode bool
ReservedExt4b bool
NoCOWFile bool
ReservedExt4c bool
UseParentProjId bool
ReservedExt2 bool
}

7
go.mod
View File

@ -1,3 +1,8 @@
module r00t2.io/sysutils

go 1.16
go 1.21

require github.com/g0rbe/go-chattr v1.0.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

2
go.sum
View File

@ -0,0 +1,2 @@
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13 h1:tgEbuE4bNVjaCWWIB1u9lDzGqH/ZdBTg33+4vNW2rjg=
github.com/johnnybubonic/go-chattr v0.0.0-20240126141003-459f46177b13/go.mod h1:yQc6VPJfpDDC1g+W2t47+yYmzBNioax/GLiyJ25/IOs=

View File

@ -20,19 +20,18 @@ package paths

import (
"errors"
`fmt`
`io/fs`
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
`strings`

"strings"
// "syscall"
)

/*
ExpandHome will take a tilde(~)-prefixed path and resolve it to the actual path in-place.
"Nested" user paths (~someuser/somechroot/~someotheruser) are not supported as home directories are expected to be absolute paths.
ExpandHome will take a tilde(~)-prefixed path and resolve it to the actual path in-place.
"Nested" user paths (~someuser/somechroot/~someotheruser) are not supported as home directories are expected to be absolute paths.
*/
func ExpandHome(path *string) (err error) {

@ -84,9 +83,80 @@ func ExpandHome(path *string) (err error) {
}

/*
MakeDirIfNotExist will create a directory at a given path if it doesn't exist.
GetFirst is the file equivalent of envs.GetFirst.

See also the documentation for RealPath.
It iterates through paths, normalizing them along the way
(so abstracted paths such as ~/foo/bar.txt and relative paths
such as bar/baz.txt will still work), and returns the content
of the first found existing file. If the first found path
is a directory, content will be nil but isDir will be true
(as will ok).

If no path exists, ok will be false.

As always, results are not guaranteed due to permissions, etc.
potentially returning an inaccurate result.

This is a thin wrapper around GetFirstWithRef.
*/
func GetFirst(paths []string) (content []byte, isDir, ok bool) {

content, isDir, ok, _ = GetFirstWithRef(paths)

return
}

/*
GetFirstWithRef is the file equivalent of envs.GetFirstWithRef.

It behaves exactly like GetFirst, but with an additional returned value, idx,
which specifies the index in paths in which a path was found.

As always, results are not guaranteed due to permissions, etc.
potentially returning an inaccurate result.
*/
func GetFirstWithRef(paths []string) (content []byte, isDir, ok bool, idx int) {

var locPaths []string
var exists bool
var stat os.FileInfo
var err error

idx = -1
// We have to be a little less cavalier about this.
if paths == nil {
return
}
locPaths = make([]string, len(paths))
locPaths = paths[:] // Create an explicit copy so we don't modify paths.
for i, p := range locPaths {
if exists, stat, err = RealPathExistsStat(&p); err != nil {
err = nil
continue
}
if !exists {
continue
}
isDir = stat.IsDir()
if !isDir {
if content, err = os.ReadFile(p); err != nil {
continue
}
}
ok = true
idx = i
return
}

return
}

/*
MakeDirIfNotExist will create a directory at a given path if it doesn't exist.

See also the documentation for RealPath.

This is a bit more sane option than os.MkdirAll as it will normalize paths a little better.
*/
func MakeDirIfNotExist(path string) (err error) {

@ -113,6 +183,8 @@ func MakeDirIfNotExist(path string) (err error) {
if !stat.Mode().IsDir() {
err = errors.New(fmt.Sprintf("path %v exists but is not a directory", locPath))
return
} else {
return
}

// This should probably never happen. Probably.
@ -121,12 +193,12 @@ func MakeDirIfNotExist(path string) (err error) {
}

/*
RealPath will transform a given path into the very best guess for an absolute path in-place.
RealPath will transform a given path into the very best guess for an absolute path in-place.

It is recommended to check err (if not nil) for an invalid path error. If this is true, the
path syntax/string itself is not supported on the runtime OS. This can be done via:
It is recommended to check err (if not nil) for an invalid path error. If this is true, the
path syntax/string itself is not supported on the runtime OS. This can be done via:

if errors.Is(err, fs.ErrInvalid) {...}
if errors.Is(err, fs.ErrInvalid) {...}
*/
func RealPath(path *string) (err error) {

@ -142,20 +214,20 @@ func RealPath(path *string) (err error) {
}

/*
RealPathExists is like RealPath, but will also return a boolean as to whether the path
actually exists or not.
RealPathExists is like RealPath, but will also return a boolean as to whether the path
actually exists or not.

Note that err *may* be os.ErrPermission/fs.ErrPermission, in which case the exists value
cannot be trusted as a permission error occurred when trying to stat the path - if the
calling user/process does not have read permission on e.g. a parent directory, then
exists may be false but the path may actually exist. This condition can be checked via
via:
Note that err *may* be os.ErrPermission/fs.ErrPermission, in which case the exists value
cannot be trusted as a permission error occurred when trying to stat the path - if the
calling user/process does not have read permission on e.g. a parent directory, then
exists may be false but the path may actually exist. This condition can be checked via
via:

if errors.Is(err, fs.ErrPermission) {...}
if errors.Is(err, fs.ErrPermission) {...}

See also the documentation for RealPath.
See also the documentation for RealPath.

In those cases, it may be preferable to use RealPathExistsStat and checking stat for nil.
In those cases, it may be preferable to use RealPathExistsStat and checking stat for nil.
*/
func RealPathExists(path *string) (exists bool, err error) {

@ -164,7 +236,7 @@ func RealPathExists(path *string) (exists bool, err error) {
}

if _, err = os.Stat(*path); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return
@ -176,11 +248,11 @@ func RealPathExists(path *string) (exists bool, err error) {
}

/*
RealPathExistsStat is like RealPathExists except it will also return the os.FileInfo
for the path (assuming it exists).
RealPathExistsStat is like RealPathExists except it will also return the os.FileInfo
for the path (assuming it exists).

If stat is nil, it is highly recommended to check err via the methods suggested
in the documentation for RealPath and RealPathExists.
If stat is nil, it is highly recommended to check err via the methods suggested
in the documentation for RealPath and RealPathExists.
*/
func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) {