From 241a46c9b4270cffc722c98f54d7f3639b58a3a7 Mon Sep 17 00:00:00 2001 From: brent saner Date: Sat, 6 Dec 2025 19:19:53 -0500 Subject: [PATCH] commit this WIP stuff to a feature branch --- envs/consts.go | 17 + envs/funcs.go | 398 ++++-------------- envs/funcs_staticenv.go | 697 ++++++++++++++++++++++++++++++++ envs/funcs_staticenv_nix.go | 47 +++ envs/funcs_staticenv_windows.go | 26 ++ envs/types.go | 31 +- internal/mem/consts.go | 1 + internal/mem/funcs.go | 81 ++++ 8 files changed, 981 insertions(+), 317 deletions(-) create mode 100644 envs/funcs_staticenv.go create mode 100644 envs/funcs_staticenv_nix.go create mode 100644 envs/funcs_staticenv_windows.go create mode 100644 internal/mem/consts.go create mode 100644 internal/mem/funcs.go diff --git a/envs/consts.go b/envs/consts.go index 6320645..2f70d41 100644 --- a/envs/consts.go +++ b/envs/consts.go @@ -1,5 +1,22 @@ package envs +import ( + "os" + + "github.com/shirou/gopsutil/v4/process" + "r00t2.io/sysutils/internal" +) + var ( StructTagInterpolate string = "envsub" ) + +var ( + defEnv *StaticEnv = &StaticEnv{ + dynamic: true, + self: true, + // don't need a process.NewProcess since the only extra thing it does is check if the PID exists. + proc: &process.Process{Pid: int32(os.Getpid())}, + envVars: internal.EnvListToMap(os.Environ()), + } +) diff --git a/envs/funcs.go b/envs/funcs.go index eda66c1..6e82ec7 100644 --- a/envs/funcs.go +++ b/envs/funcs.go @@ -1,17 +1,88 @@ package envs import ( + "math" "os" "reflect" "strings" - "sync" - "r00t2.io/goutils/multierr" - "r00t2.io/goutils/structutils" + "github.com/shirou/gopsutil/v4/process" "r00t2.io/sysutils/errs" "r00t2.io/sysutils/internal" ) +/* +Current returns a *copy* of the StaticEnv for this current process' environment. + +It is set to dynamically refresh with strictRefresh mode set to false. +(see [NewEnvFromPid] docs for what these do/mean.) +Assuming permissions haven't wildly gone silly during runtime, it shouldn't ever +have issues with dynamic refreshing. +It will never panic regardless. +*/ +func Current() (s *StaticEnv) { + + s = &StaticEnv{ + dynamic: defEnv.dynamic, + envVars: defEnv.GetEnvMap(), + } + for k, v := range defEnv.envVars { + s.envVars[k] = v + } + + return +} + +/* +NewEnvFromMap returns a [StaticEnv] from a fixed list of environment variables. +This is primarily useful for mocking and other tests. +*/ +func NewEnvFromMap(envMap map[string]string) (s *StaticEnv, err error) { + + if envMap == nil { + err = errs.ErrNilPtr + } + + s = &StaticEnv{ + envVars: envMap, + } + + return +} + +/* +NewEnvFromPid returns a [StaticEnv] from a given PID. + +If dynamicRefresh is true, the env vars will be refreshed from the process +on every method call. +Note that this will obviously cause errors/panics if the process it binds to disappears +during runtime, +or +*/ +func NewEnvFromPid(pid uint, dynamicRefresh, strictRefresh bool) (s *StaticEnv, err error) { + + if pid > math.MaxInt32 { + err = errs.ErrHighPid + return + } + + s = &StaticEnv{ + dynamic: dynamicRefresh, + } + + if s.proc, err = process.NewProcess(int32(pid)); err != nil { + return + } + if err = s.Refresh(); err != nil { + return + } + // Test the ability to attach to procs. + err = s.platChecks() + s.strict = strictRefresh + + return +} + /* DefEnv operates like Python's .get() method on dicts (maps); if the environment variable specified by key does not exist/is not specified, @@ -98,6 +169,18 @@ func GetEnvMap() (envVars map[string]string) { return } +// GetEnvMust wraps GetEnvErr but will panic on error. +func GetEnvMust(key string) (value string) { + + var err error + + if value, err = GetEnvErr(key); err != nil { + panic(err) + } + + 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. @@ -331,312 +414,3 @@ func InterpolateString(s *string) (err error) { return } - -// interpolateMap is used by [Interpolate] for maps. v should be a [reflect.Value] of a map. -func interpolateMap(v reflect.Value) (err error) { - - var kVal reflect.Value - var vVal reflect.Value - var newMap reflect.Value - var wg sync.WaitGroup - var numJobs int - var errChan chan error - var doneChan chan bool = make(chan bool, 1) - var mErr *multierr.MultiError = multierr.NewMultiError(nil) - var t reflect.Type = v.Type() - var kind reflect.Kind = t.Kind() - - if kind != reflect.Map { - err = errs.ErrBadType - return - } - - if v.IsNil() || v.IsZero() || !v.IsValid() { - return - } - - numJobs = v.Len() - errChan = make(chan error, numJobs) - wg.Add(numJobs) - - newMap = reflect.MakeMap(v.Type()) - - for _, kVal = range v.MapKeys() { - vVal = v.MapIndex(kVal) - go func(key, val reflect.Value) { - var mapErr error - var newKey reflect.Value - var newVal reflect.Value - - newKey = reflect.New(key.Type()).Elem() - newVal = reflect.New(val.Type()).Elem() - - newKey.Set(key.Convert(newKey.Type())) - newVal.Set(val.Convert(newVal.Type())) - - defer wg.Done() - - // key - if key.Kind() == reflect.String { - if mapErr = interpolateStringReflect(newKey); mapErr != nil { - errChan <- mapErr - return - } - } else { - if mapErr = interpolateValue(newKey); mapErr != nil { - errChan <- mapErr - return - } - } - // value - if val.Kind() == reflect.String { - if mapErr = interpolateStringReflect(newVal); mapErr != nil { - errChan <- mapErr - return - } - } else { - if mapErr = interpolateValue(newVal); mapErr != nil { - errChan <- mapErr - return - } - } - - newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type())) - }(kVal, vVal) - } - - go func() { - wg.Wait() - close(errChan) - doneChan <- true - }() - - <-doneChan - - for i := 0; i < numJobs; i++ { - if err = <-errChan; err != nil { - mErr.AddError(err) - err = nil - } - } - - if !mErr.IsEmpty() { - err = mErr - return - } - - v.Set(newMap.Convert(v.Type())) - - return -} - -// interpolateSlice is used by [Interpolate] for slices and arrays. v should be a [reflect.Value] of a slice/array. -func interpolateSlice(v reflect.Value) (err error) { - - var wg sync.WaitGroup - var errChan chan error - var numJobs int - var doneChan chan bool = make(chan bool, 1) - var mErr *multierr.MultiError = multierr.NewMultiError(nil) - var t reflect.Type = v.Type() - var kind reflect.Kind = t.Kind() - - switch kind { - case reflect.Slice: - if v.IsNil() || v.IsZero() || !v.IsValid() { - return - } - case reflect.Array: - if v.IsZero() || !v.IsValid() { - return - } - default: - err = errs.ErrBadType - return - } - - numJobs = v.Len() - errChan = make(chan error, numJobs) - wg.Add(numJobs) - - for i := 0; i < v.Len(); i++ { - go func(idx int) { - var sErr error - - defer wg.Done() - - if v.Index(idx).Kind() == reflect.String { - if sErr = interpolateStringReflect(v.Index(idx)); sErr != nil { - errChan <- sErr - return - } - } else { - if sErr = interpolateValue(v.Index(idx)); sErr != nil { - errChan <- sErr - return - } - } - }(i) - } - - go func() { - wg.Wait() - close(errChan) - doneChan <- true - }() - - <-doneChan - - for i := 0; i < numJobs; i++ { - if err = <-errChan; err != nil { - mErr.AddError(err) - err = nil - } - } - - if !mErr.IsEmpty() { - err = mErr - return - } - - return -} - -// interpolateStringReflect is used for structs/nested strings using reflection. -func interpolateStringReflect(v reflect.Value) (err error) { - - var strVal string - - if v.Kind() != reflect.String { - err = errs.ErrBadType - return - } - - if strVal, err = interpolateString(v.String()); err != nil { - return - } - - v.Set(reflect.ValueOf(strVal).Convert(v.Type())) - - return -} - -// interpolateStruct is used by [Interpolate] for structs. v should be a [reflect.Value] of a struct. -func interpolateStruct(v reflect.Value) (err error) { - - var field reflect.StructField - var fieldVal reflect.Value - var wg sync.WaitGroup - var errChan chan error - var numJobs int - var doneChan chan bool = make(chan bool, 1) - var mErr *multierr.MultiError = multierr.NewMultiError(nil) - var t reflect.Type = v.Type() - var kind reflect.Kind = t.Kind() - - if kind != reflect.Struct { - err = errs.ErrBadType - return - } - - numJobs = v.NumField() - wg.Add(numJobs) - errChan = make(chan error, numJobs) - - for i := 0; i < v.NumField(); i++ { - field = t.Field(i) - fieldVal = v.Field(i) - - go func(f reflect.StructField, fv reflect.Value) { - var fErr error - - defer wg.Done() - - if fErr = interpolateStructField(f, fv); fErr != nil { - errChan <- fErr - return - } - }(field, fieldVal) - } - - go func() { - wg.Wait() - close(errChan) - doneChan <- true - }() - - <-doneChan - - for i := 0; i < numJobs; i++ { - if err = <-errChan; err != nil { - mErr.AddError(err) - err = nil - } - } - - if !mErr.IsEmpty() { - err = mErr - return - } - - return -} - -// interpolateStructField interpolates a struct field. -func interpolateStructField(field reflect.StructField, v reflect.Value) (err error) { - - var parsedTagOpts map[string]bool - - if !v.CanSet() { - return - } - - // Skip if explicitly instructed to do so. - parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim) - if parsedTagOpts["-"] { - return - } - - if v.Kind() == reflect.Ptr { - err = interpolateStructField(field, v.Elem()) - } else { - err = interpolateValue(v) - } - - return -} - -// interpolateValue is a dispatcher for a reflect value. -func interpolateValue(v reflect.Value) (err error) { - - var kind reflect.Kind = v.Kind() - - switch kind { - case reflect.Ptr: - if v.IsNil() || v.IsZero() || !v.IsValid() { - return - } - v = v.Elem() - if err = interpolateValue(v); err != nil { - return - } - case reflect.String: - if err = interpolateStringReflect(v); err != nil { - return - } - return - case reflect.Slice, reflect.Array: - if err = interpolateSlice(v); err != nil { - } - case reflect.Map: - if err = interpolateMap(v); err != nil { - return - } - case reflect.Struct: - if err = interpolateStruct(v); err != nil { - return - } - } - - return -} diff --git a/envs/funcs_staticenv.go b/envs/funcs_staticenv.go new file mode 100644 index 0000000..a345d80 --- /dev/null +++ b/envs/funcs_staticenv.go @@ -0,0 +1,697 @@ +package envs + +import ( + "os" + "reflect" + "strings" + "sync" + + "r00t2.io/goutils/multierr" + "r00t2.io/goutils/structutils" + "r00t2.io/sysutils/errs" + "r00t2.io/sysutils/internal" +) + +/* +DefEnv operates like Python's .get() method on dicts (maps); +if the environment variable specified by key does not exist/is not specified, +then the value specified by fallback will be returned instead +otherwise key's value is returned. +*/ +func (s *StaticEnv) DefEnv(key, fallback string) (value string) { + + var exists bool + + if value, exists = os.LookupEnv(key); !exists { + value = fallback + } + + return +} + +// DefEnvBlank is like [DefEnv] but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*. +func (s *StaticEnv) DefEnvBlank(key, fallback string) (value string) { + + value = DefEnv(key, fallback) + if value == "" { + value = fallback + } + + return +} + +// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an [EnvErrNoVal]. +func (s *StaticEnv) GetEnvErr(key string) (value string, err error) { + + var exists bool + + if value, exists = os.LookupEnv(key); !exists { + err = &EnvErrNoVal{ + VarName: key, + } + return + } + + return +} + +/* +GetEnvErrNoBlank behaves exactly like [GetEnvErr] with the +additional stipulation that the value must not be empty. + +An error for a value that is non-empty but whitespace only (e.g. VARNM="\t") +can be returned if ignoreWhitespace == true. + +(If it is, an [EnvErrNoVal] will also be returned.) +*/ +func (s *StaticEnv) GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err error) { + + var exists bool + var e *EnvErrNoVal = &EnvErrNoVal{ + VarName: key, + WasRequiredNonEmpty: true, + IgnoreWhitespace: ignoreWhitespace, + } + + if value, exists = os.LookupEnv(key); !exists { + err = e + return + } else { + e.WasFound = true + e.WasWhitespace = (strings.TrimSpace(value) == "") && (value != "") + if ignoreWhitespace && e.WasWhitespace { + err = e + return + } + } + + return +} + +// GetEnvMap returns a map of all environment variables. All values are strings. +func (s *StaticEnv) GetEnvMap() (envVars map[string]string) { + + var envList []string = os.Environ() + + envVars = internal.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 (s *StaticEnv) GetEnvMapNative() (envMap map[string]interface{}) { + + var stringMap map[string]string = GetEnvMap() + + envMap = internal.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 (s *StaticEnv) 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 (s *StaticEnv) 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 +} + +// GetPathEnv returns a slice of the PATH variable's items. +func (s *StaticEnv) GetPathEnv() (pathList []string, err error) { + + if pathList, err = internal.GetPathEnv(); err != nil { + return + } + + return +} + +/* +GetPidEnvMap will only work on *NIX-like systems with procfs. +It gets the environment variables of a given process' PID. +*/ +func (s *StaticEnv) GetPidEnvMap(pid uint32) (envMap map[string]string, err error) { + + if envMap, err = internal.GetPidEnvMap(pid); err != nil { + return + } + + return +} + +/* +GetPidEnvMapNative returns a map of all environment variables (like [GetEnvMapNative]), 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 (s *StaticEnv) GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) { + + var stringMap map[string]string + + if stringMap, err = internal.GetPidEnvMap(pid); err != nil { + return + } + + envMap = internal.NativizeEnvMap(stringMap) + + return +} + +/* +HasEnv is much like [os.LookupEnv], but only returns a boolean indicating +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 (s *StaticEnv) HasEnv(key string) (envIsSet bool) { + + _, envIsSet = os.LookupEnv(key) + + return +} + +/* +Interpolate takes one of: + + - a string (pointer only) + - a struct (pointer only) + - a map (applied to both keys *and* values) + - a slice + +and performs variable substitution on strings from environment variables. + +It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and, +if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%). + +For structs, the tag name used can be changed by setting the [StructTagInterpolate] +variable in this submodule; the default is `envsub`. +If the tag value is "-", the field will be skipped. +For map fields within structs etc., the default is to apply interpolation to both keys and values. +All other tag value(s) are ignored. + +For maps and slices, Interpolate will recurse into values (e.g. [][]string will work as expected). + +If s is nil, no interpolation will be performed. No error will be returned. +If s is not a valid/supported type, no interpolation will be performed. No error will be returned. +*/ +func (s *StaticEnv) Interpolate(inStr any) (err error) { + + var ptrVal reflect.Value + var ptrType reflect.Type + var ptrKind reflect.Kind + var sVal reflect.Value = reflect.ValueOf(inStr) + var sType reflect.Type = sVal.Type() + var kind reflect.Kind = sType.Kind() + + switch kind { + case reflect.Ptr: + if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() { + return + } + ptrVal = sVal.Elem() + ptrType = ptrVal.Type() + ptrKind = ptrType.Kind() + if ptrKind == reflect.String { + err = s.interpolateStringReflect(ptrVal) + } else { + // Otherwise, it should be a struct ptr. + if ptrKind != reflect.Struct { + return + } + err = s.interpolateStruct(ptrVal) + } + case reflect.Map: + if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() { + return + } + err = s.interpolateMap(sVal) + case reflect.Slice: + if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() { + return + } + err = s.interpolateSlice(sVal) + /* + case reflect.Struct: + if sVal.IsZero() || !sVal.IsValid() { + return + } + err = interpolateStruct(sVal) + + */ + } + + return +} + +/* +InterpolateString takes (a pointer to) a struct or string and performs variable substitution on it +from environment variables. + +It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and, +if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%). + +If s is nil, nothing will be done and err will be [errs.ErrNilPtr]. + +This is a standalone function that is much more performant than [Interpolate] +at the cost of rigidity. +*/ +func (s *StaticEnv) InterpolateString(inStr *string) (err error) { + + var newStr string + + if inStr == nil { + err = errs.ErrNilPtr + return + } + + if newStr, err = interpolateString(*inStr); err != nil { + return + } + *inStr = newStr + + return +} + +/* +Refresh refreshes the current environment. + +This is called automatically on all other methods if +the [StaticEnv] was allocated with `dynamic` set to true. +*/ +func (s *StaticEnv) Refresh() (err error) { + + var ev []string + + if !s.self && s.proc == nil { + // NO-OP; static mapping + return + } + + if s.self { + ev = os.Environ() + } else if s.proc != nil { + if ev, err = s.proc.Environ(); err != nil { + return + } + } + s.envVars = internal.EnvListToMap(ev) + + return +} + +/* +Set sets variable key to value. + +This only works under the following conditions: + + - [SetEnv] was created from [NewEnvFromMap] + - [SetEnv] is from [Current] + - [SetEnv] is from [NewEnvFromPid] *and* + this process has PTRACE permissions/capabilities on/for + the target PID's process. +*/ +func (s *StaticEnv) Set(key string, value string) { + + var err error + + s.lock.Lock() + defer s.lock.Unlock() + + s.envVars[key] = value + if s.proc != nil { + if err = s.setProcVal(key, value); err != nil && s.strict { + panic(err) + } + } + + return +} + +// interpolateMap is used by [Interpolate] for maps. v should be a [reflect.Value] of a map. +func (s *StaticEnv) interpolateMap(v reflect.Value) (err error) { + + var kVal reflect.Value + var vVal reflect.Value + var newMap reflect.Value + var wg sync.WaitGroup + var numJobs int + var errChan chan error + var doneChan chan bool = make(chan bool, 1) + var mErr *multierr.MultiError = multierr.NewMultiError(nil) + var t reflect.Type = v.Type() + var kind reflect.Kind = t.Kind() + + if kind != reflect.Map { + err = errs.ErrBadType + return + } + + if v.IsNil() || v.IsZero() || !v.IsValid() { + return + } + + numJobs = v.Len() + errChan = make(chan error, numJobs) + wg.Add(numJobs) + + newMap = reflect.MakeMap(v.Type()) + + for _, kVal = range v.MapKeys() { + vVal = v.MapIndex(kVal) + go func(key, val reflect.Value) { + var mapErr error + var newKey reflect.Value + var newVal reflect.Value + + newKey = reflect.New(key.Type()).Elem() + newVal = reflect.New(val.Type()).Elem() + + newKey.Set(key.Convert(newKey.Type())) + newVal.Set(val.Convert(newVal.Type())) + + defer wg.Done() + + // key + if key.Kind() == reflect.String { + if mapErr = s.interpolateStringReflect(newKey); mapErr != nil { + errChan <- mapErr + return + } + } else { + if mapErr = s.interpolateValue(newKey); mapErr != nil { + errChan <- mapErr + return + } + } + // value + if val.Kind() == reflect.String { + if mapErr = s.interpolateStringReflect(newVal); mapErr != nil { + errChan <- mapErr + return + } + } else { + if mapErr = s.interpolateValue(newVal); mapErr != nil { + errChan <- mapErr + return + } + } + + newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type())) + }(kVal, vVal) + } + + go func() { + wg.Wait() + close(errChan) + doneChan <- true + }() + + <-doneChan + + for i := 0; i < numJobs; i++ { + if err = <-errChan; err != nil { + mErr.AddError(err) + err = nil + } + } + + if !mErr.IsEmpty() { + err = mErr + return + } + + v.Set(newMap.Convert(v.Type())) + + return +} + +// interpolateSlice is used by [Interpolate] for slices and arrays. v should be a [reflect.Value] of a slice/array. +func (s *StaticEnv) interpolateSlice(v reflect.Value) (err error) { + + var wg sync.WaitGroup + var errChan chan error + var numJobs int + var doneChan chan bool = make(chan bool, 1) + var mErr *multierr.MultiError = multierr.NewMultiError(nil) + var t reflect.Type = v.Type() + var kind reflect.Kind = t.Kind() + + switch kind { + case reflect.Slice: + if v.IsNil() || v.IsZero() || !v.IsValid() { + return + } + case reflect.Array: + if v.IsZero() || !v.IsValid() { + return + } + default: + err = errs.ErrBadType + return + } + + numJobs = v.Len() + errChan = make(chan error, numJobs) + wg.Add(numJobs) + + for i := 0; i < v.Len(); i++ { + go func(idx int) { + var sErr error + + defer wg.Done() + + if v.Index(idx).Kind() == reflect.String { + if sErr = s.interpolateStringReflect(v.Index(idx)); sErr != nil { + errChan <- sErr + return + } + } else { + if sErr = s.interpolateValue(v.Index(idx)); sErr != nil { + errChan <- sErr + return + } + } + }(i) + } + + go func() { + wg.Wait() + close(errChan) + doneChan <- true + }() + + <-doneChan + + for i := 0; i < numJobs; i++ { + if err = <-errChan; err != nil { + mErr.AddError(err) + err = nil + } + } + + if !mErr.IsEmpty() { + err = mErr + return + } + + return +} + +// interpolateStringReflect is used for structs/nested strings using reflection. +func (s *StaticEnv) interpolateStringReflect(v reflect.Value) (err error) { + + var strVal string + + if v.Kind() != reflect.String { + err = errs.ErrBadType + return + } + + if strVal, err = interpolateString(v.String()); err != nil { + return + } + + v.Set(reflect.ValueOf(strVal).Convert(v.Type())) + + return +} + +// interpolateStruct is used by [Interpolate] for structs. v should be a [reflect.Value] of a struct. +func (s *StaticEnv) interpolateStruct(v reflect.Value) (err error) { + + var field reflect.StructField + var fieldVal reflect.Value + var wg sync.WaitGroup + var errChan chan error + var numJobs int + var doneChan chan bool = make(chan bool, 1) + var mErr *multierr.MultiError = multierr.NewMultiError(nil) + var t reflect.Type = v.Type() + var kind reflect.Kind = t.Kind() + + if kind != reflect.Struct { + err = errs.ErrBadType + return + } + + numJobs = v.NumField() + wg.Add(numJobs) + errChan = make(chan error, numJobs) + + for i := 0; i < v.NumField(); i++ { + field = t.Field(i) + fieldVal = v.Field(i) + + go func(f reflect.StructField, fv reflect.Value) { + var fErr error + + defer wg.Done() + + if fErr = s.interpolateStructField(f, fv); fErr != nil { + errChan <- fErr + return + } + }(field, fieldVal) + } + + go func() { + wg.Wait() + close(errChan) + doneChan <- true + }() + + <-doneChan + + for i := 0; i < numJobs; i++ { + if err = <-errChan; err != nil { + mErr.AddError(err) + err = nil + } + } + + if !mErr.IsEmpty() { + err = mErr + return + } + + return +} + +// interpolateStructField interpolates a struct field. +func (s *StaticEnv) interpolateStructField(field reflect.StructField, v reflect.Value) (err error) { + + var parsedTagOpts map[string]bool + + if !v.CanSet() { + return + } + + // Skip if explicitly instructed to do so. + parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim) + if parsedTagOpts["-"] { + return + } + + if v.Kind() == reflect.Ptr { + err = s.interpolateStructField(field, v.Elem()) + } else { + err = s.interpolateValue(v) + } + + return +} + +// interpolateValue is a dispatcher for a reflect value. +func (s *StaticEnv) interpolateValue(v reflect.Value) (err error) { + + var kind reflect.Kind = v.Kind() + + switch kind { + case reflect.Ptr: + if v.IsNil() || v.IsZero() || !v.IsValid() { + return + } + v = v.Elem() + if err = s.interpolateValue(v); err != nil { + return + } + case reflect.String: + if err = s.interpolateStringReflect(v); err != nil { + return + } + return + case reflect.Slice, reflect.Array: + if err = s.interpolateSlice(v); err != nil { + } + case reflect.Map: + if err = s.interpolateMap(v); err != nil { + return + } + case reflect.Struct: + if err = s.interpolateStruct(v); err != nil { + return + } + } + + return +} diff --git a/envs/funcs_staticenv_nix.go b/envs/funcs_staticenv_nix.go new file mode 100644 index 0000000..c8e8be4 --- /dev/null +++ b/envs/funcs_staticenv_nix.go @@ -0,0 +1,47 @@ +//go:build !(windows || plan9 || wasip1 || js || ios) + +package envs + +import ( + "golang.org/x/sys/unix" +) + +func (s *StaticEnv) platChecks() (err error) { + + if s.proc != nil && !s.self { + // Check for ptrace caps/perms + if err = unix.PtraceAttach(int(s.proc.Pid)); err != nil { + return + } + if err = unix.PtraceDetach(int(s.proc.Pid)); err != nil { + return + } + } + + return +} + +func (s *StaticEnv) setProcVal(key, value string) (err error) { + + if s.self { + if err = unix.Setenv(key, value); err != nil { + return + } + } else { + if err = unix.PtraceAttach(int(s.proc.Pid)); err != nil { + return + } + if err = unix.PtraceDetach(int(s.proc.Pid)); err != nil { + return + } + } + + return +} + +func (s *StaticEnv) unsetProcVal(key string) (err error) { + + err = unix.Unsetenv(key) + + return +} diff --git a/envs/funcs_staticenv_windows.go b/envs/funcs_staticenv_windows.go new file mode 100644 index 0000000..52fe1fe --- /dev/null +++ b/envs/funcs_staticenv_windows.go @@ -0,0 +1,26 @@ +//go:build windows + +package envs + +import ( + "golang.org/x/sys/windows" +) + +func (s *StaticEnv) platChecks() (err error) { + + return +} + +func (s *StaticEnv) setProcVal(key, value string) (err error) { + + err = windows.Setenv(key, value) + + return +} + +func (s *StaticEnv) unsetProcVal(key string) (err error) { + + err = windows.Unsetenv(key) + + return +} diff --git a/envs/types.go b/envs/types.go index 44f45f7..c212d12 100644 --- a/envs/types.go +++ b/envs/types.go @@ -1,5 +1,11 @@ package envs +import ( + "sync" + + "github.com/shirou/gopsutil/v4/process" +) + type ( /* EnvErrNoVal is an error containing the variable that does not exist @@ -7,14 +13,29 @@ type ( */ EnvErrNoVal struct { // VarName is the variable name/key name originally specified in the function call. - VarName string `json:"var" toml:"VariableName" yaml:"Variable Name/Key" xml:"key,attr"` + VarName string `json:"var" toml:"VariableName" yaml:"Variable Name/Key" xml:"key,attr"` // WasFound is only used for GetEnvErrNoBlank(). It is true if the variable was found/populated. - WasFound bool `json:"found" toml:"Found" yaml:"Found" xml:"found,attr"` + WasFound bool `json:"found" toml:"Found" yaml:"Found" xml:"found,attr"` // WasRequiredNonEmpty indicates that this error was returned in a context where a variable was required to be non-empty (e.g. via GetEnvErrNoBlank()) but was empty. - WasRequiredNonEmpty bool `json:"reqd_non_empty" toml:"RequiredNonEmpty" yaml:"Required Non-Empty" xml:"reqNonEmpty,attr"` + WasRequiredNonEmpty bool `json:"reqd_non_empty" toml:"RequiredNonEmpty" yaml:"Required Non-Empty" xml:"reqNonEmpty,attr"` // IgnoreWhitespace is true if the value was found but its evaluation was done against a whitestripped version. - IgnoreWhitespace bool `json:"ignore_ws" toml:"IgnoreWhitespace" yaml:"Ignore Whitespace" xml:"ignoreWhitespace,attr"` + IgnoreWhitespace bool `json:"ignore_ws" toml:"IgnoreWhitespace" yaml:"Ignore Whitespace" xml:"ignoreWhitespace,attr"` // WasWhitespace is true if the value was whitespace-only. - WasWhitespace bool `json:"was_ws" toml:"WasWhitespace" yaml:"Was Whitespace Only" xml:"wasWhitespace,attr"` + WasWhitespace bool `json:"was_ws" toml:"WasWhitespace" yaml:"Was Whitespace Only" xml:"wasWhitespace,attr"` + } + + /* + StaticEnv is an environment variable mapping that duplicates the normal functions of the standalone functions + but can be used for other processes, mock tests, etc. + + (The standalone functions actually perform the same functions on the default StaticEnv as returned from [Current].) + */ + StaticEnv struct { + dynamic bool + strict bool + envVars map[string]string + self bool + proc *process.Process + lock sync.RWMutex } ) diff --git a/internal/mem/consts.go b/internal/mem/consts.go new file mode 100644 index 0000000..6a9fb31 --- /dev/null +++ b/internal/mem/consts.go @@ -0,0 +1 @@ +package mem diff --git a/internal/mem/funcs.go b/internal/mem/funcs.go new file mode 100644 index 0000000..7879835 --- /dev/null +++ b/internal/mem/funcs.go @@ -0,0 +1,81 @@ +package mem + +import ( + "encoding/binary" + "unsafe" + + "golang.org/x/sys/unix" +) + +func getStdRIP(r *unix.PtraceRegs) (rip uint64) { + + return +} + +func getRIP(r *unix.PtraceRegs) uint64 { + // amd64 + if binary.Size(unix.PtraceRegs{}) == 216 { + return r.Rip + } + // arm64 + type regsArm64 struct{ Regs [18]uint64 } + return (*regsArm64)(unsafe.Pointer(r)).Regs[16] // PC +} + +func setRIP(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs { + if binary.Size(unix.PtraceRegs{}) == 216 { + r.Rip = v + } else { + type regsArm64 struct{ Regs [18]uint64 } + (*regsArm64)(unsafe.Pointer(r)).Regs[16] = v + } + return r +} + +func getRSP(r *unix.PtraceRegs) uint64 { + if binary.Size(unix.PtraceRegs{}) == 216 { + return r.Rsp + } + type regsArm64 struct{ Regs [18]uint64 } + return (*regsArm64)(unsafe.Pointer(r)).Regs[17] // SP +} + +func setRSP(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs { + if binary.Size(unix.PtraceRegs{}) == 216 { + r.Rsp = v + } else { + type regsArm64 struct{ Regs [18]uint64 } + (*regsArm64)(unsafe.Pointer(r)).Regs[17] = v + } + return r +} + +func setArg0(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs { + if binary.Size(unix.PtraceRegs{}) == 216 { + r.Rdi = v + } else { + type regsArm64 struct{ Regs [18]uint64 } + (*regsArm64)(unsafe.Pointer(r)).Regs[0] = v // X0 + } + return r +} + +func setArg1(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs { + if binary.Size(unix.PtraceRegs{}) == 216 { + r.Rsi = v + } else { + type regsArm64 struct{ Regs [18]uint64 } + (*regsArm64)(unsafe.Pointer(r)).Regs[1] = v // X1 + } + return r +} + +func setArg2(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs { + if binary.Size(unix.PtraceRegs{}) == 216 { + r.Rdx = v + } else { + type regsArm64 struct{ Regs [18]uint64 } + (*regsArm64)(unsafe.Pointer(r)).Regs[2] = v // X2 + } + return r +}