Compare commits

...

9 Commits

30 changed files with 982 additions and 458 deletions

View File

@ -34,8 +34,8 @@ package main
import (
`fmt`
`r00t2.io/sysutils/net/ports`
`r00t2.io/sysutils/net/protos`
`r00t2.io/sysutils/.net.UNFINISHED/ports`
`r00t2.io/sysutils/.net.UNFINISHED/protos`
)

func main() {
@ -77,8 +77,8 @@ import (
`fmt`
`log`

`r00t2.io/sysutils/net/ports`
`r00t2.io/sysutils/net/protos`
`r00t2.io/sysutils/.net.UNFINISHED/ports`
`r00t2.io/sysutils/.net.UNFINISHED/protos`
)

func main() {
@ -114,7 +114,7 @@ import (
`fmt`
`log`

`r00t2.io/sysutils/net/ports`
`r00t2.io/sysutils/.net.UNFINISHED/ports`
)

func main() {
@ -148,7 +148,7 @@ package main
import (
`fmt`

`r00t2.io/sysutils/net/protos`
`r00t2.io/sysutils/.net.UNFINISHED/protos`
)

func main() {
@ -184,7 +184,7 @@ import (
`fmt`
`log`

`r00t2.io/sysutils/net/protos`
`r00t2.io/sysutils/.net.UNFINISHED/protos`
)

func main() {
@ -217,7 +217,7 @@ import (
`fmt`
`log`

`r00t2.io/sysutils/net/protos`
`r00t2.io/sysutils/.net.UNFINISHED/protos`
)

func main() {

View File

@ -5,7 +5,7 @@ import (
"io"
"net/http"

"r00t2.io/sysutils/net/ports"
_ "r00t2.io/sysutils/.net.UNFINISHED/ports"
)

func download(url string) (b *[]byte, err error) {

View File

@ -8,10 +8,11 @@ import (
"strconv"
"strings"

// https://pkg.go.dev/github.com/jszwec/csvutil but I can't seem to fetch it.
"github.com/jszwec/csvutil"

"r00t2.io/sysutils/net/ports"
"r00t2.io/sysutils/net/protos"
"r00t2.io/sysutils/.net.UNFINISHED/ports"
"r00t2.io/sysutils/.net.UNFINISHED/protos"
"r00t2.io/sysutils/paths"
)

View File

@ -1,7 +1,7 @@
package ports

import (
"r00t2.io/sysutils/net/protos"
"r00t2.io/sysutils/.net.UNFINISHED/protos"
)

type IPPort struct {

30
TODO Normal file
View File

@ -0,0 +1,30 @@
- password generator utility/library
-- 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
-- https://support.microsoft.com/en-us/topic/2dc5c4b9-0881-2e0a-48df-f120493a2d3e
-- https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/-2000-server/cc959336(v=technet.10)?redirectedfrom=MSDN
-- https://stackoverflow.com/questions/33078854/what-is-the-regex-for-windows-domain-username-in-c

- finish net

60
envs/.TODO.go.UNFINISHED Normal file
View File

@ -0,0 +1,60 @@
package envs

/*
EnvMapper contains the environment variables as grouped by their basic type.
If a variable's type cannot be determined, it's placed in Strings.
If a variable's type is a list, it will be an []interface{} as each item may be a different variable type.
It essentially is the same as EnvMap except with the types split out for convenience.
*/
type EnvMapper struct {
Booleans map[string]bool `json:"bools"`
Numbers map[string]int `json:"nums"`
Strings map[string]string `json:"strings"`
Lists map[string][]interface{} `json:"lists"`
}

// GetEnvMapper returns a pointer to a populated EnvMapper.
func GetEnvMapper() (e *EnvMapper, err error) {

var em map[string]interface{}
var env EnvMapper

env = EnvMapper{
Booleans: nil,
Numbers: nil,
Strings: nil,
Lists: nil,
}

for k, v := range em {

switch t := v.(type) {
case bool:
if env.Booleans == nil {
env.Booleans = make(map[string]bool, 0)
}
env.Booleans[k] = t
continue
case int:
if env.Numbers == nil {
env.Numbers = make(map[string]int, 0)
}
env.Numbers[k] = t
continue
case []interface{}:
if env.Lists == nil {
env.Lists = make(map[string][]interface{}, 0)
}
env.Lists[k] = t
case string: // the "default" since everything could probably be typeswitched to a string otherwise.
if env.Strings == nil {
env.Strings = make(map[string]string, 0)
}
env.Strings[k] = t
}
}

*e = env

return
}

11
envs/consts.go Normal file
View File

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

import (
"regexp"
)

// Compiled regex patterns.
var (
reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`)
reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`)
)

187
envs/funcs.go Normal file
View File

@ -0,0 +1,187 @@
package envs

import (
`bytes`
`errors`
`fmt`
`io/ioutil`
`os`
`strings`

`r00t2.io/sysutils/internal`
`r00t2.io/sysutils/paths`
)

// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (pathList []string, err error) {

var pathVar string = internal.GetPathEnvName()

pathList = make([]string, 0)

for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
if err = paths.RealPath(&p); err != nil {
return
}
pathList = append(pathList, p)
}
return
}

// GetEnvMap returns a map of all environment variables. All values are strings.
func GetEnvMap() (envVars map[string]string) {

var envList []string = os.Environ()

envVars = envListToMap(envList)

return
}

/*
GetEnvMapNative returns a map of all environment variables, but attempts to "nativize" them.
All values are interfaces. It is up to the caller to typeswitch them to proper types.

Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
a []string (as per GetPathEnv). No other env vars, even if they contain os.PathListSeparator,
will be transformed to a slice or the like.
If an error occurs during parsing the path env var, it will be rendered as a string.

All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).

If a type cannot be determined for a value, its string form will be used
(as it would be found in GetEnvMap).
*/
func GetEnvMapNative() (envMap map[string]interface{}) {

var stringMap map[string]string = GetEnvMap()

envMap = nativizeEnvMap(stringMap)

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.
*/
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {

var envBytes []byte
var envList []string
var envArr [][]byte
var procPath string
var exists bool

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

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

if exists, err = paths.RealPathExists(&procPath); err != nil {
return
}
if !exists {
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
return
}

if envBytes, err = ioutil.ReadFile(procPath); err != nil {
return
}

envArr = bytes.Split(envBytes, []byte{0x0})
envList = make([]string, len(envArr))
for idx, b := range envArr {
envList[idx] = string(b)
}

envMap = envListToMap(envList)

return
}

/*
GetPidEnvMapNative, like GetEnvMapNative, returns a map of all environment variables, but attempts to "nativize" them.
All values are interfaces. It is up to the caller to typeswitch them to proper types.

See the documentation for GetEnvMapNative for details.
*/
func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {

var stringMap map[string]string

if stringMap, err = GetPidEnvMap(pid); err != nil {
return
}

envMap = nativizeEnvMap(stringMap)

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
}

87
envs/utils.go Normal file
View File

@ -0,0 +1,87 @@
package envs

import (
`strconv`
`strings`

`r00t2.io/sysutils/internal`
)

// envListToMap splits a []string of env var keypairs to a map.
func envListToMap(envs []string) (envMap map[string]string) {

var kv []string
var k, v string

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

for _, ev := range envs {
kv = strings.SplitN(ev, "=", 2)
// I *think* SplitN does this for me, but...
if len(kv) == 1 {
kv = append(kv, "")
}
k = kv[0]
v = kv[1]
envMap[k] = v
}

return
}

// nativizeEnvMap returns a native-typed env map from a string version.
func nativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{}) {

var pathVar string = internal.GetPathEnvName()
var err error

envMap = make(map[string]interface{}, 0)

for k, v := range stringMap {

// Check for PATH/Path - we handle this uniquely.
if k == pathVar {
if envMap[k], err = GetPathEnv(); err != nil {
envMap[k] = v
err = nil
}
continue
}

// It might be...
// a float
if reMaybeFloat.MatchString(v) {
if envMap[k], err = strconv.ParseFloat(v, 64); err == nil {
continue
}
err = nil
}

// an int
if reMaybeInt.MatchString(v) {
if envMap[k], err = strconv.Atoi(v); err == nil {
continue
}
err = nil
}

// a uint
if envMap[k], err = strconv.ParseUint(v, 10, 64); err == nil {
continue
} else {
err = nil
}

// a boolean
if envMap[k], err = strconv.ParseBool(v); err == nil {
continue
} else {
err = nil
}

// ok so... guess it's a string, then.
envMap[k] = v
}

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
}

264
funcs.go
View File

@ -1,264 +0,0 @@
package sysutils

import (
`bytes`
`errors`
`fmt`
`io/ioutil`
`os`
`runtime`
`strconv`
`strings`

`r00t2.io/sysutils/paths`
)

/*
EnvMapper contains the environment variables as grouped by their basic type.
If a variable's type cannot be determined, it's placed in Strings.
If a variable's type is a list, it will be an []interface{} as each item may be a different variable type.
It essentially is the same as EnvMap except with the types split out for convenience.
*/
type EnvMapper struct {
Booleans map[string]bool `json:"bools"`
Numbers map[string]int `json:"nums"`
Strings map[string]string `json:"strings"`
Lists map[string][]interface{} `json:"lists"`
}

// GetEnvMapper returns a pointer to a populated EnvMapper.
func GetEnvMapper() (e *EnvMapper, err error) {

var em map[string]interface{}
var env EnvMapper

env = EnvMapper{
Booleans: nil,
Numbers: nil,
Strings: nil,
Lists: nil,
}

for k, v := range em {

switch t := v.(type) {
case bool:
if env.Booleans == nil {
env.Booleans = make(map[string]bool, 0)
}
env.Booleans[k] = t
continue
case int:
if env.Numbers == nil {
env.Numbers = make(map[string]int, 0)
}
env.Numbers[k] = t
continue
case []interface{}:
if env.Lists == nil {
env.Lists = make(map[string][]interface{}, 0)
}
env.Lists[k] = t
case string: // the "default" since everything could probably be typeswitched to a string otherwise.
if env.Strings == nil {
env.Strings = make(map[string]string, 0)
}
env.Strings[k] = t
}
}

*e = env

return
}

/*
EnvMap returns a map of environment variables.
The variable is an interface due to it attempting to use a variable's value to its "true" native type.
If a type cannot be determined, it will be treated a string.
*/
func EnvMap() (envs map[string]interface{}, err error) {

var ems map[string]string

if ems, err = EnvMapString(); err != nil {
return
}

for k, v := range ems {

// Is int?
if i, ok := getNum(v); ok {
envs[k] = i
continue
}

// Is bool?
if b, ok := getBool(v); ok {
envs[k] = b
continue
}

// Is array?
if a, ok := getArr(v); ok {
envs[k] = a
continue
}

// It's a string (probably).
envs[k] = v
}

return
}

// EnvMapString is like EnvMap, but all values are treated as strings.
func EnvMapString() (envs map[string]string, err error) {

var envArray []string

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

envArray = os.Environ()

for _, e := range envArray {
var k, v string
var kv []string

kv = strings.SplitN(e, "=", 2)
k = kv[0]
if len(kv) == 2 {
v = kv[1]
} else {
v = ""
}
envs[k] = v
}

return
}

// UTILITY FUNCS

// getBool attempts to convert a string value to a boolean.
func getBool(s string) (b bool, ok bool) {

switch s2 := strings.ToLower(strings.TrimSpace(s)); s2 {
case "true", "yes", "y":
b = true
ok = true
case "false", "no", "n":
b = false
ok = true
}

return
}

// getNum attempts to convert a string value to an int.
func getNum(s string) (n int, ok bool) {

var err error

if n, err = strconv.Atoi(s); err == nil {
ok = true
}

return
}

// getArr attempts to convert a string value to an array of interface{}.
func getArr(s string) (a []interface{}, ok bool) {

var arrS []string

if arrS, ok = getArrStr(s); !ok {
return
}

a = make([]interface{}, len(arrS))

for idx, i := range arrS {
if b, ok := getBool(i); ok {
a[idx] = b
} else if n, ok := getNum(i); ok {
a[idx] = n
} else if l, ok := getArr(i); ok {
a[idx] = l
} else {
a[idx] = i
}
}

return
}

// getArrStr attempts to convert a string value to an array of strings.
func getArrStr(s string) (a []string, ok bool) {

var sep string = ":"

if runtime.GOOS == "windows" {
sep = ";"
}

a = strings.Split(s, sep)
l := len(s)
if l <= 1 {
return
}

ok = true

return
}

/*
GetEnvPid will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID.
*/
func GetEnvPid(pid uint32) (env map[string]string, err error) {

var envBytes []byte
var envArr [][]byte
var procPath string
var exists bool

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

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

if exists, err = paths.RealPathExists(&procPath); err != nil {
return
}
if !exists {
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
return
}

if envBytes, err = ioutil.ReadFile(procPath); err != nil {
return
}

envArr = bytes.Split(envBytes, []byte{0x0})

for _, b := range envArr {

// s := strings.TrimSpace(string(b))
s := string(b)
e := strings.SplitN(s, "=", 2)

for _, i := range e {

if len(i) != 2 {
env[string(i[0])] = ""
continue
}

env[string(i[0])] = string(i[1])

}
}

return
}

7
go.mod
View File

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

go 1.16
go 1.21

require github.com/jszwec/csvutil v1.5.0
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

4
go.sum
View File

@ -1,2 +1,2 @@
github.com/jszwec/csvutil v1.5.0 h1:ErLnF1Qzzt9svk8CUY7CyLl/W9eET+KWPIZWkE1o6JM=
github.com/jszwec/csvutil v1.5.0/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg=
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=

8
internal/consts.go Normal file
View File

@ -0,0 +1,8 @@
package internal

// OS-specific path environment variable name. The default is "PATH".
var (
pathEnvVarName map[string]string = map[string]string{
"windows": "Path",
}
)

18
internal/utils.go Normal file
View File

@ -0,0 +1,18 @@
package internal

import (
`runtime`
)

// GetPathEnvName gets the OS-specific path environment variable name.
func GetPathEnvName() (envVarName string) {

var ok bool

if envVarName, ok = pathEnvVarName[runtime.GOOS]; !ok {
// *NIX/the default.
envVarName = "PATH"
}

return
}

View File

@ -1,174 +0,0 @@
/*
SysUtils - a library to assist with various system-related functions
Copyright (C) 2020 Brent Saner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package paths

import (
"errors"
`fmt`
"os"
"os/user"
"path/filepath"
`runtime`
// "strconv"
"strings"

// "syscall"
)

/*
ExpandHome will take a tilde(~)-prefixed path and resolve it to the actual path in-place.
Note that it only works for current user; the syntax ~someotheruser/foo/bar is currently unsupported.
*/
func ExpandHome(path *string) (err error) {

var usr *user.User

// Props to this guy.
// https://stackoverflow.com/a/43578461/733214
if len(*path) == 0 {
err = errors.New("empty path")
return
} else if (*path)[0] != '~' {
return
}

// E(ffective)UID (e.g. chown'd user for SUID)
/*
uid := strconv.Itoa(syscall.Geteuid())
usr, err := user.LookupId(euid)
*/
// (Real)UID (invoking user)
if usr, err = user.Current(); err != nil {
return err
}
*path = filepath.Join(usr.HomeDir, (*path)[1:])

return
}

// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (paths []string, err error) {

var pathVar string = "PATH"
var sep string = ":"

paths = make([]string, 0)

if runtime.GOOS == "windows" {
pathVar = "Path"
sep = ";"
}

for _, p := range strings.Split(os.Getenv(pathVar), sep) {
if err = RealPath(&p); err != nil {
return
}
paths = append(paths, p)
}
return
}

// MakeDirIfNotExist will create a directory at a given path if it doesn't exist.
func MakeDirIfNotExist(path string) (err error) {

var stat os.FileInfo
var exists bool
var locPath string = path

if exists, stat, err = RealPathExistsStat(&locPath); err != nil {
if !exists {
// This, at least as of golang 1.15, uses the user's umask.
// It does not actually create a dir with 0777.
// It's up to the caller to do an os.Chmod() on the path after, if desired.
if err = os.MkdirAll(locPath, 0777); err != nil {
return
}
err = nil
return
} else {
return
}
}

// So it exists, but it probably isn't a dir.
if !stat.Mode().IsDir() {
err = errors.New(fmt.Sprintf("path %v exists but is not a directory", locPath))
return
}

// This should probably never happen. Probably.
err = errors.New("undefined")
return
}

// RealPath will transform a given path into the very best guess for an absolute path in-place.
func RealPath(path *string) (err error) {

if err = ExpandHome(path); err != nil {
return
}

if *path, err = filepath.Abs(*path); err != nil {
return
}

return
}

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

It's hacky, but the "exists" bool along with err is a sort of proto-state-machine.
If err != nil and bool is true, the error occurred during path absolution.
If err != nil and bool is false, the path does not exist.
*/
func RealPathExists(path *string) (exists bool, err error) {

if err = RealPath(path); err != nil {
exists = true
return
}

if _, err = os.Stat(*path); err != nil {
return
}

exists = true

return
}

// RealPathExistsStat is like RealPathExists except it will also return the os.FileInfo for the path (assuming it exists).
func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) {

// See the comments for RealPathExists for details on this.
if err = RealPath(path); err != nil {
exists = true
return
}

if stat, err = os.Stat(*path); err != nil {
return
}

exists = true

return
}

268
paths/funcs.go Normal file
View File

@ -0,0 +1,268 @@
/*
SysUtils - a library to assist with various system-related functions
Copyright (C) 2020 Brent Saner

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package paths

import (
"errors"
"fmt"
"io/fs"
"os"
"os/user"
"path/filepath"
"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.
*/
func ExpandHome(path *string) (err error) {

var unameSplit []string
var uname string

var u *user.User

// Props to this guy.
// https://stackoverflow.com/a/43578461/733214
if len(*path) == 0 {
err = errors.New("empty path")
return
} else if (*path)[0] != '~' {
return
}

// E(ffective)UID (e.g. chown'd user for SUID)
/*
uid := strconv.Itoa(syscall.Geteuid())
u, err := user.LookupId(euid)
*/
// (Real)UID (invoking user)
/*
if u, err = user.Current(); err != nil {
return
}
*/
// K but do it smarter.
unameSplit = strings.SplitN(*path, string(os.PathSeparator), 2)
if len(unameSplit) != 2 {
unameSplit = append(unameSplit, "")
}

uname = strings.TrimPrefix(unameSplit[0], "~")
if uname == "" {
if u, err = user.Current(); err != nil {
return
}
} else {
if u, err = user.Lookup(uname); err != nil {
return
}
}

*path = filepath.Join(u.HomeDir, unameSplit[1])

return
}

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

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) {

var stat os.FileInfo
var exists bool
var locPath string = path

if exists, stat, err = RealPathExistsStat(&locPath); err != nil {
if !exists {
// This, at least as of golang 1.15, uses the user's umask.
// It does not actually create a dir with 0777.
// It's up to the caller to do an os.Chmod() on the path after, if desired.
if err = os.MkdirAll(locPath, 0777); err != nil {
return
}
err = nil
return
} else {
return
}
}

// So it exists, but it probably isn't a dir.
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.
err = errors.New("undefined")
return
}

/*
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:

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

if err = ExpandHome(path); err != nil {
return
}

if *path, err = filepath.Abs(*path); err != nil {
return
}

return
}

/*
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:

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

See also the documentation for RealPath.

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

if err = RealPath(path); err != nil {
return
}

if _, err = os.Stat(*path); err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = nil
}
return
}

exists = true

return
}

/*
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.
*/
func RealPathExistsStat(path *string) (exists bool, stat os.FileInfo, err error) {

if exists, err = RealPathExists(path); err != nil {
return
}

if stat, err = os.Stat(*path); err != nil {
return
}

return
}

View File

@ -16,12 +16,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/


package terminal

import (
"os"
"fmt"
)

// IsShell returns true if the program is running inside an interactive shell (interactive invocation, sudo, etc.), and false if not (cron, ssh exec, pipe, etc.).
@ -32,8 +30,8 @@ func IsShell() (interactive bool) {

stdoutStat, _ = os.Stdout.Stat()

if (stdoutStaf.Mode() & os.ModeCharDevice) != 0 {
interactive = True
if (stdoutStat.Mode() & os.ModeCharDevice) != 0 {
interactive = true
}

return