diff --git a/errkit/README.md b/errkit/README.md new file mode 100644 index 0000000..4279e06 --- /dev/null +++ b/errkit/README.md @@ -0,0 +1,58 @@ +# errkit + +why errkit when we already have errorutil ? + +---- + +Introduced a year ago, `errorutil` aimed to capture error stacks for identifying deeply nested errors. However, its approach deviates from Go's error handling paradigm. In Go, libraries like "errors", "pkg/errors", and "uber.go/multierr" avoid using the `.Error()` method directly. Instead, they wrap errors with helper structs that implement specific interfaces, facilitating error chain traversal and the use of helper functions like `.Cause() error` or `.Unwrap() error` or `errors.Is()`. Contrarily, `errorutil` marshals errors to strings, which is incompatible with Go's error handling paradigm. Over time, the use of `errorutil` has become cumbersome due to its inability to replace any error package seamlessly and its lack of support for idiomatic error propagation or traversal in Go. + + +`errkit` is a new error library that addresses the shortcomings of `errorutil`. It offers the following features: + +- Seamless replacement for existing error packages, requiring no syntax changes or refactoring: + - `errors` package + - `pkg/errors` package (now deprecated) + - `uber/multierr` package +- `errkit` is compatible with all known Go error handling implementations. It can parse errors from any library and works with existing error handling libraries and helper functions like `Is()`, `As()`, `Cause()`, and more. +- `errkit` is Go idiomatic and adheres to the Go error handling paradigm. +- `errkit` supports attributes for structured error information or logging using `slog.Attr` (optional). +- `errkit` implements and categorizes errors into different kinds, as detailed below. + - `ErrKindNetworkTemporary` + - `ErrKindNetworkPermanent` + - `ErrKindDeadline` + - Custom kinds via `ErrKind` interface +- `errkit` provides helper functions for structured error logging using `SlogAttrs` and `SlogAttrGroup`. +- `errkit` offers helper functions to implement public or user-facing errors by using error kinds interface. + + +**Attributes Support** + +`errkit` supports optional error wrapping with attributes `slog.Attr` for structured error logging, providing a more organized approach to error logging than string wrapping. + +```go +// normal way of error propogating through nested stack +err := errkit.New("i/o timeout") + +// xyz.go +err := errkit.Wrap(err,"failed to connect %s",addr) + +// abc.go +err := errkit.Wrap(err,"error occured when downloading %s",xyz) +``` + +with attributes support you can do following + +```go +// normal way of error propogating through nested stack +err := errkit.New("i/o timeout") + +// xyz.go +err = errkit.WithAttr(err,slog.Any("resource",domain)) + +// abc.go +err = errkit.WithAttr(err,slog.Any("action","download")) +``` + +## Note + +To keep errors concise and avoid unnecessary allocations, message wrapping and attributes count have a max depth set to 3. Adding more will not panic but will be simply ignored. This is configurable using the MAX_ERR_DEPTH env variable (default 3). \ No newline at end of file diff --git a/errkit/errors.go b/errkit/errors.go new file mode 100644 index 0000000..fe93b63 --- /dev/null +++ b/errkit/errors.go @@ -0,0 +1,278 @@ +// errkit implements all errors generated by nuclei and includes error definations +// specific to nuclei , error classification (like network,logic) etc +package errkit + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + + "github.com/projectdiscovery/utils/env" + "golang.org/x/exp/maps" +) + +const ( + // DelimArrow is delim used by projectdiscovery/utils to join errors + DelimArrow = "<-" + // DelimArrowSerialized + DelimArrowSerialized = "\u003c-" + // DelimSemiColon is standard delim popularly used to join errors + DelimSemiColon = "; " + // DelimMultiLine is delim used to join errors in multiline format + DelimMultiLine = "\n - " + // MultiLinePrefix is the prefix used for multiline errors + MultiLineErrPrefix = "the following errors occurred:" +) + +var ( + // MaxErrorDepth is the maximum depth of errors to be unwrapped or maintained + // all errors beyond this depth will be ignored + MaxErrorDepth = env.GetEnvOrDefault("MAX_ERROR_DEPTH", 3) + // ErrorSeperator is the seperator used to join errors + ErrorSeperator = env.GetEnvOrDefault("ERROR_SEPERATOR", "; ") +) + +// ErrorX is a custom error type that can handle all known types of errors +// wrapping and joining strategies including custom ones and it supports error class +// which can be shown to client/users in more meaningful way +type ErrorX struct { + kind ErrKind + attrs map[string]slog.Attr + errs []error + uniqErrs map[string]struct{} +} + +// append is internal method to append given +// error to error slice , it removes duplicates +func (e *ErrorX) append(errs ...error) { + if e.uniqErrs == nil { + e.uniqErrs = make(map[string]struct{}) + } + for _, err := range errs { + if _, ok := e.uniqErrs[err.Error()]; ok { + continue + } + e.uniqErrs[err.Error()] = struct{}{} + e.errs = append(e.errs, err) + } +} + +// Errors returns all errors parsed by the error +func (e *ErrorX) Errors() []error { + return e.errs +} + +// Attrs returns all attributes associated with the error +func (e *ErrorX) Attrs() []slog.Attr { + if e.attrs == nil { + return nil + } + return maps.Values(e.attrs) +} + +// Build returns the object as error interface +func (e *ErrorX) Build() error { + return e +} + +// Unwrap returns the underlying error +func (e *ErrorX) Unwrap() []error { + return e.errs +} + +// Is checks if current error contains given error +func (e *ErrorX) Is(err error) bool { + x := &ErrorX{} + parseError(x, err) + // even one submatch is enough + for _, orig := range e.errs { + for _, match := range x.errs { + if errors.Is(orig, match) { + return true + } + } + } + return false +} + +// MarshalJSON returns the json representation of the error +func (e *ErrorX) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{ + "kind": e.kind.String(), + "errors": e.errs, + } + if len(e.attrs) > 0 { + m["attrs"] = slog.GroupValue(maps.Values(e.attrs)...) + } + return json.Marshal(m) +} + +// Error returns the error string +func (e *ErrorX) Error() string { + var sb strings.Builder + if e.kind != nil && e.kind.String() != "" { + sb.WriteString("errKind=") + sb.WriteString(e.kind.String()) + sb.WriteString(" ") + } + if len(e.attrs) > 0 { + sb.WriteString(slog.GroupValue(maps.Values(e.attrs)...).String()) + sb.WriteString(" ") + } + for _, err := range e.errs { + sb.WriteString(err.Error()) + sb.WriteString(ErrorSeperator) + } + return strings.TrimSuffix(sb.String(), ErrorSeperator) +} + +// Cause return the original error that caused this without any wrapping +func (e *ErrorX) Cause() error { + if len(e.errs) > 0 { + return e.errs[0] + } + return nil +} + +// Kind returns the errorkind associated with this error +// if any +func (e *ErrorX) Kind() ErrKind { + if e.kind == nil || e.kind.String() == "" { + return ErrKindUnknown + } + return e.kind +} + +// FromError parses a given error to understand the error class +// and optionally adds given message for more info +func FromError(err error) *ErrorX { + if err == nil { + return nil + } + nucleiErr := &ErrorX{} + parseError(nucleiErr, err) + return nucleiErr +} + +// New creates a new error with the given message +func New(format string, args ...interface{}) *ErrorX { + return &ErrorX{errs: []error{fmt.Errorf(format, args...)}} +} + +// Msgf adds a message to the error +func (e *ErrorX) Msgf(format string, args ...interface{}) { + if e == nil { + return + } + e.append(fmt.Errorf(format, args...)) +} + +// SetClass sets the class of the error +// if underlying error class was already set, then it is given preference +// when generating final error msg +func (e *ErrorX) SetKind(kind ErrKind) *ErrorX { + if e.kind == nil { + e.kind = kind + } else { + e.kind = CombineErrKinds(e.kind, kind) + } + return e +} + +// SetAttr sets additional attributes to a given error +// it only adds unique attributes and ignores duplicates +// Note: only key is checked for uniqueness +func (e *ErrorX) SetAttr(s ...slog.Attr) *ErrorX { + for _, attr := range s { + if e.attrs == nil { + e.attrs = make(map[string]slog.Attr) + } + // check if this exists + if _, ok := e.attrs[attr.Key]; !ok && len(e.attrs) < MaxErrorDepth { + e.attrs[attr.Key] = attr + } + } + return e +} + +// parseError recursively parses all known types of errors +func parseError(to *ErrorX, err error) { + if err == nil { + return + } + if to == nil { + to = &ErrorX{} + } + if len(to.errs) >= MaxErrorDepth { + return + } + + switch v := err.(type) { + case *ErrorX: + to.append(v.errs...) + to.kind = CombineErrKinds(to.kind, v.kind) + case JoinedError: + foundAny := false + for _, e := range v.Unwrap() { + to.append(e) + foundAny = true + } + if !foundAny { + parseError(to, errors.New(err.Error())) + } + case WrappedError: + if v.Unwrap() != nil { + parseError(to, v.Unwrap()) + } else { + parseError(to, errors.New(err.Error())) + } + case CauseError: + to.append(v.Cause()) + remaining := strings.Replace(err.Error(), v.Cause().Error(), "", -1) + parseError(to, errors.New(remaining)) + default: + errString := err.Error() + // try assigning to enriched error + if strings.Contains(errString, DelimArrow) { + // Split the error by arrow delim + parts := strings.Split(errString, DelimArrow) + for i := len(parts) - 1; i >= 0; i-- { + part := strings.TrimSpace(parts[i]) + parseError(to, errors.New(part)) + } + } else if strings.Contains(errString, DelimArrowSerialized) { + // Split the error by arrow delim + parts := strings.Split(errString, DelimArrowSerialized) + for i := len(parts) - 1; i >= 0; i-- { + part := strings.TrimSpace(parts[i]) + parseError(to, errors.New(part)) + } + } else if strings.Contains(errString, DelimSemiColon) { + // Split the error by semi-colon delim + parts := strings.Split(errString, DelimSemiColon) + for _, part := range parts { + part = strings.TrimSpace(part) + parseError(to, errors.New(part)) + } + } else if strings.Contains(errString, MultiLineErrPrefix) { + // remove prefix + msg := strings.ReplaceAll(errString, MultiLineErrPrefix, "") + parts := strings.Split(msg, DelimMultiLine) + for _, part := range parts { + part = strings.TrimSpace(part) + parseError(to, errors.New(part)) + } + } else { + // this cannot be furthur unwrapped + to.append(err) + } + } +} + +// WrappedError is implemented by errors that are wrapped +type WrappedError interface { + // Unwrap returns the underlying error + Unwrap() error +} diff --git a/errkit/errors_test.go b/errkit/errors_test.go new file mode 100644 index 0000000..f83db4a --- /dev/null +++ b/errkit/errors_test.go @@ -0,0 +1,107 @@ +package errkit + +import ( + "testing" + + "github.com/pkg/errors" + errorutil "github.com/projectdiscovery/utils/errors" + "github.com/stretchr/testify/require" + "go.uber.org/multierr" + + stderrors "errors" +) + +// what are these tests ? +// Below tests check for interoperability of this package with other error packages +// like pkg/errors and go.uber.org/multierr and std errors as well + +func TestErrorAs(t *testing.T) { + // Create a new error with a specific class and wrap it + x := New("this is a nuclei error").SetKind(ErrKindNetworkPermanent).Build() + y := errors.Wrap(x, "this is a wrap error") + + // Attempt to unwrap the error to a specific type + ne := &ErrorX{} + if !errors.As(y, &ne) { + t.Fatal("expected to be able to unwrap") + } + + // Wrap the specific error type into another error and try unwrapping again + wrapped := Wrap(ne, "this is a wrapped error") + if !errors.As(wrapped, &ne) { + t.Fatal("expected to be able to unwrap") + } + + // Combine multiple errors into a multierror and attempt to unwrap to the specific type + errs := []error{ + stderrors.New("this is a std error"), + x, + errors.New("this is a pkg error"), + } + multi := multierr.Combine(errs...) + if !errors.As(multi, &ne) { + t.Fatal("expected to be able to unwrap") + } +} + +func TestErrorIs(t *testing.T) { + // Create a new error, wrap it, and check if the original error can be found + x := New("this is a nuclei error").SetKind(ErrKindNetworkPermanent).Build() + y := errors.Wrap(x, "this is a wrap error") + if !errors.Is(y, x) { + t.Fatal("expected to be able to find the original error") + } + + // Wrap the original error with a custom wrapper and check again + wrapped := Wrap(x, "this is a wrapped error") + if !stderrors.Is(wrapped, x) { + t.Fatal("expected to be able to find the original error") + } + + // Combine multiple errors into a multierror and check if the original error can be found + errs := []error{ + stderrors.New("this is a std error"), + x, + errors.New("this is a pkg error"), + } + multi := multierr.Combine(errs...) + if !errors.Is(multi, x) { + t.Fatal("expected to be able to find the original error") + } +} + +func TestErrorUtil(t *testing.T) { + utilErr := errorutil.New("got err while executing http://206.189.19.240:8000/wp-content/plugins/wp-automatic/inc/csv.php <- POST http://206.189.19.240:8000/wp-content/plugins/wp-automatic/inc/csv.php giving up after 2 attempts: Post \"http://206.189.19.240:8000/wp-content/plugins/wp-automatic/inc/csv.php\": [:RUNTIME] ztls fallback failed <- dial tcp 206.189.19.240:8000: connect: connection refused") + x := ErrorX{} + parseError(&x, utilErr) + if len(x.errs) != 3 { + t.Fatal("expected 3 errors") + } +} + +func TestErrKindCheck(t *testing.T) { + x := New("port closed or filtered").SetKind(ErrKindNetworkPermanent) + t.Run("Errkind With Normal Error", func(t *testing.T) { + wrapped := Wrap(x, "this is a wrapped error") + if !IsKind(wrapped, ErrKindNetworkPermanent) { + t.Fatal("expected to be able to find the original error") + } + }) + + // mix of multiple kinds + tmp := New("i/o timeout").SetKind(ErrKindNetworkTemporary) + t.Run("Errkind With Multiple Kinds", func(t *testing.T) { + wrapped := Append(x, tmp) + errx := FromError(wrapped) + val, ok := errx.kind.(*multiKind) + require.True(t, ok, "expected to be able to find the original error") + require.Equal(t, 2, len(val.kinds)) + }) + + // duplicate kinds + t.Run("Errkind With Duplicate Kinds", func(t *testing.T) { + wrapped := Append(x, x) + errx := FromError(wrapped) + require.True(t, errx.kind.Is(ErrKindNetworkPermanent), "expected to be able to find the original error") + }) +} diff --git a/errkit/helpers.go b/errkit/helpers.go new file mode 100644 index 0000000..c1016a0 --- /dev/null +++ b/errkit/helpers.go @@ -0,0 +1,280 @@ +package errkit + +import ( + "errors" + "log/slog" +) + +// Proxy to StdLib errors.Is +func Is(err error, target ...error) bool { + if err == nil { + return false + } + for i := range target { + t := target[i] + if errors.Is(err, t) { + return true + } + } + return false +} + +// IsKind checks if given error is equal to one of the given errkind +// if error did not already have a kind, it tries to parse it +// using default error kinds and given kinds +func IsKind(err error, match ...ErrKind) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + // try to parse kind from error + if x.kind == nil { + // parse kind from error + tmp := []ErrKind{ErrKindDeadline, ErrKindNetworkPermanent, ErrKindNetworkTemporary} + tmp = append(tmp, match...) + x.kind = GetErrorKind(err, tmp...) + } + if x.kind != nil { + if val, ok := x.kind.(*multiKind); ok && len(val.kinds) > 0 { + // if multi kind return first kind + for _, kind := range val.kinds { + for _, k := range match { + if k.Is(kind) { + return true + } + } + } + } + for _, kind := range match { + if kind.Is(x.kind) { + return true + } + } + } + return false +} + +// Proxy to StdLib errors.As +func As(err error, target interface{}) bool { + return errors.As(err, target) +} + +// Combine combines multiple errors into a single error +func Combine(errs ...error) error { + if len(errs) == 0 { + return nil + } + x := &ErrorX{} + for _, err := range errs { + if err == nil { + continue + } + parseError(x, err) + } + return x +} + +// Wrap wraps the given error with the message +func Wrap(err error, message string) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(message) + return x +} + +// Wrapf wraps the given error with the message +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(format, args...) + return x +} + +// Errors returns all underlying errors there were appended or joined +func Errors(err error) []error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + return x.errs +} + +// Append appends given errors and returns a new error +// it ignores all nil errors +func Append(errs ...error) error { + if len(errs) == 0 { + return nil + } + x := &ErrorX{} + for _, err := range errs { + if err == nil { + continue + } + parseError(x, err) + } + return x +} + +// Join joins given errors and returns a new error +// it ignores all nil errors +// Note: unlike Other libraries, Join does not use `\n` +// so it is equivalent to wrapping/Appending errors +func Join(errs ...error) error { + return Append(errs...) +} + +// Cause returns the original error that caused this error +func Cause(err error) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + return x.Cause() +} + +// WithMessage +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(message) + return x +} + +// WithMessagef +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + x.Msgf(format, args...) + return x +} + +// IsNetworkTemporaryErr checks if given error is a temporary network error +func IsNetworkTemporaryErr(err error) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + return isNetworkTemporaryErr(x) +} + +// IsDeadlineErr checks if given error is a deadline error +func IsDeadlineErr(err error) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + return isDeadlineErr(x) +} + +// IsNetworkPermanentErr checks if given error is a permanent network error +func IsNetworkPermanentErr(err error) bool { + if err == nil { + return false + } + x := &ErrorX{} + parseError(x, err) + return isNetworkPermanentErr(x) +} + +// WithAttr wraps error with given attributes +// +// err = errkit.WithAttr(err,slog.Any("resource",domain)) +func WithAttr(err error, attrs ...slog.Attr) error { + if err == nil { + return nil + } + if len(attrs) == 0 { + return err + } + x := &ErrorX{} + parseError(x, err) + return x.SetAttr(attrs...) +} + +// GetAttr returns all attributes of given error if it has any +func GetAttr(err error) []slog.Attr { + if err == nil { + return nil + } + x := &ErrorX{} + parseError(x, err) + return x.Attrs() +} + +// ToSlogAttrGroup returns a slog attribute group for the given error +// it is in format of: +// +// { +// "data": { +// "kind": "", +// "cause": "", +// "errors": [ +// ... +// ] +// } +// } +func ToSlogAttrGroup(err error) slog.Attr { + attrs := ToSlogAttrs(err) + g := slog.GroupValue( + attrs..., // append all attrs + ) + return slog.Any("data", g) +} + +// ToSlogAttrs returns slog attributes for the given error +// it is in format of: +// +// { +// "kind": "", +// "cause": "", +// "errors": [ +// ... +// ] +// } +func ToSlogAttrs(err error) []slog.Attr { + x := &ErrorX{} + parseError(x, err) + attrs := []slog.Attr{} + if x.kind != nil { + attrs = append(attrs, slog.Any("kind", x.kind.String())) + } + if cause := x.Cause(); cause != nil { + attrs = append(attrs, slog.Any("cause", cause)) + } + if len(x.errs) > 0 { + attrs = append(attrs, slog.Any("errors", x.errs)) + } + return attrs +} + +// GetAttrValue returns the value of the attribute with given key +func GetAttrValue(err error, key string) slog.Value { + if err == nil { + return slog.Value{} + } + x := &ErrorX{} + parseError(x, err) + for _, attr := range x.attrs { + if attr.Key == key { + return attr.Value + } + } + return slog.Value{} +} diff --git a/errkit/interfaces.go b/errkit/interfaces.go new file mode 100644 index 0000000..950efb7 --- /dev/null +++ b/errkit/interfaces.go @@ -0,0 +1,32 @@ +package errkit + +import "encoding/json" + +var ( + _ json.Marshaler = &ErrorX{} + _ JoinedError = &ErrorX{} + _ CauseError = &ErrorX{} + _ ComparableError = &ErrorX{} + _ error = &ErrorX{} +) + +// below contains all interfaces that are implemented by ErrorX which +// makes it compatible with other error packages + +// JoinedError is implemented by errors that are joined by Join +type JoinedError interface { + // Unwrap returns the underlying error + Unwrap() []error +} + +// CauseError is implemented by errors that have a cause +type CauseError interface { + // Cause return the original error that caused this without any wrapping + Cause() error +} + +// ComparableError is implemented by errors that can be compared +type ComparableError interface { + // Is checks if current error contains given error + Is(err error) bool +} diff --git a/errkit/kind.go b/errkit/kind.go new file mode 100644 index 0000000..9e3cd6c --- /dev/null +++ b/errkit/kind.go @@ -0,0 +1,299 @@ +package errkit + +import ( + "context" + "errors" + "os" + "strings" + + "golang.org/x/exp/maps" +) + +var ( + // ErrClassNetwork indicates an error related to network operations + // these may be resolved by retrying the operation with exponential backoff + // ex: Timeout awaiting headers, i/o timeout etc + ErrKindNetworkTemporary = NewPrimitiveErrKind("network-temporary-error", "temporary network error", isNetworkTemporaryErr) + // ErrKindNetworkPermanent indicates a permanent error related to network operations + // these may not be resolved by retrying and need manual intervention + // ex: no address found for host + ErrKindNetworkPermanent = NewPrimitiveErrKind("network-permanent-error", "permanent network error", isNetworkPermanentErr) + // ErrKindDeadline indicates a timeout error in logical operations + // these are custom deadlines set by nuclei itself to prevent infinite hangs + // and in most cases are server side issues (ex: server connects but does not respond at all) + // a manual intervention is required + ErrKindDeadline = NewPrimitiveErrKind("deadline-error", "deadline error", isDeadlineErr) + // ErrKindUnknown indicates an unknown error class + // that has not been implemented yet this is used as fallback when converting a slog Item + ErrKindUnknown = NewPrimitiveErrKind("unknown-error", "unknown error", nil) +) + +var ( + // DefaultErrorKinds is the default error kinds used in classification + // if one intends to add more default error kinds it must be done in init() function + // of that package to avoid race conditions + DefaultErrorKinds = []ErrKind{ + ErrKindNetworkTemporary, + ErrKindNetworkPermanent, + ErrKindDeadline, + } +) + +// ErrKind is an interface that represents a kind of error +type ErrKind interface { + // Is checks if current error kind is same as given error kind + Is(ErrKind) bool + // IsParent checks if current error kind is parent of given error kind + // this allows heirarchical classification of errors and app specific handling + IsParent(ErrKind) bool + // RepresentsError checks if given error is of this kind + Represents(*ErrorX) bool + // Description returns predefined description of the error kind + // this can be used to show user friendly error messages in case of error + Description() string + // String returns the string representation of the error kind + String() string +} + +var _ ErrKind = &primitiveErrKind{} + +// primitiveErrKind is kind of error used in classification +type primitiveErrKind struct { + id string + info string + represents func(*ErrorX) bool +} + +func (e *primitiveErrKind) Is(kind ErrKind) bool { + return e.id == kind.String() +} + +func (e *primitiveErrKind) IsParent(kind ErrKind) bool { + return false +} + +func (e *primitiveErrKind) Represents(err *ErrorX) bool { + if e.represents != nil { + return e.represents(err) + } + return false +} + +func (e *primitiveErrKind) String() string { + return e.id +} + +func (e *primitiveErrKind) Description() string { + return e.info +} + +// NewPrimitiveErrKind creates a new primitive error kind +func NewPrimitiveErrKind(id string, info string, represents func(*ErrorX) bool) ErrKind { + p := &primitiveErrKind{id: id, info: info, represents: represents} + return p +} + +func isNetworkTemporaryErr(err *ErrorX) bool { + if err.Cause() != nil { + return os.IsTimeout(err.Cause()) + } + v := err.Cause() + switch { + case os.IsTimeout(v): + return true + case strings.Contains(v.Error(), "Client.Timeout exceeded while awaiting headers"): + return true + } + return false +} + +// isNetworkPermanentErr checks if given error is a permanent network error +func isNetworkPermanentErr(err *ErrorX) bool { + if err.Cause() == nil { + return false + } + v := err.Cause().Error() + // to implement + switch { + case strings.Contains(v, "no address found"): + return true + case strings.Contains(v, "no such host"): + return true + case strings.Contains(v, "could not resolve host"): + return true + case strings.Contains(v, "port closed or filtered"): + // pd standard error for port closed or filtered + return true + case strings.Contains(v, "connect: connection refused"): + return true + } + return false +} + +// isDeadlineErr checks if given error is a deadline error +func isDeadlineErr(err *ErrorX) bool { + // to implement + if err.Cause() == nil { + return false + } + v := err.Cause() + switch { + case errors.Is(v, os.ErrDeadlineExceeded): + return true + case errors.Is(v, context.DeadlineExceeded): + return true + case errors.Is(v, context.Canceled): + return true + } + + return false +} + +type multiKind struct { + kinds []ErrKind +} + +func (e *multiKind) Is(kind ErrKind) bool { + for _, k := range e.kinds { + if k.Is(kind) { + return true + } + } + return false +} + +func (e *multiKind) IsParent(kind ErrKind) bool { + for _, k := range e.kinds { + if k.IsParent(kind) { + return true + } + } + return false +} + +func (e *multiKind) Represents(err *ErrorX) bool { + for _, k := range e.kinds { + if k.Represents(err) { + return true + } + } + return false +} + +func (e *multiKind) String() string { + var str string + for _, k := range e.kinds { + str += k.String() + "," + } + return strings.TrimSuffix(str, ",") +} + +func (e *multiKind) Description() string { + var str string + for _, k := range e.kinds { + str += k.Description() + "\n" + } + return strings.TrimSpace(str) +} + +// CombineErrKinds combines multiple error kinds into a single error kind +// this is not recommended but available if needed +// It is currently used in ErrorX while printing the error +// It is recommended to implement a hierarchical error kind +// instead of using this outside of errkit +func CombineErrKinds(kind ...ErrKind) ErrKind { + // while combining it also consolidates child error kinds into parent + // but note it currently does not support deeply nested childs + // and can only consolidate immediate childs + f := &multiKind{} + uniq := map[ErrKind]struct{}{} + for _, k := range kind { + if k == nil || k.String() == "" { + continue + } + if val, ok := k.(*multiKind); ok { + for _, v := range val.kinds { + uniq[v] = struct{}{} + } + } else { + uniq[k] = struct{}{} + } + } + all := maps.Keys(uniq) + for _, k := range all { + for u := range uniq { + if k.IsParent(u) { + delete(uniq, u) + } + } + } + if len(uniq) > 1 { + // check and remove unknown error kind + for k := range uniq { + if k.Is(ErrKindUnknown) { + delete(uniq, k) + } + } + } + + f.kinds = maps.Keys(uniq) + if len(f.kinds) > MaxErrorDepth { + f.kinds = f.kinds[:MaxErrorDepth] + } + if len(f.kinds) == 1 { + return f.kinds[0] + } + return f +} + +// GetErrorKind returns the first error kind from the error +// extra error kinds can be passed as optional arguments +func GetErrorKind(err error, defs ...ErrKind) ErrKind { + x := &ErrorX{} + parseError(x, err) + if x.kind != nil { + if val, ok := x.kind.(*multiKind); ok && len(val.kinds) > 0 { + // if multi kind return first kind + return val.kinds[0] + } + return x.kind + } + // if no kind is found return default error kind + // parse if defs are given + for _, def := range defs { + if def.Represents(x) { + return def + } + } + // check in default error kinds + for _, def := range DefaultErrorKinds { + if def.Represents(x) { + return def + } + } + return ErrKindUnknown +} + +// GetAllErrorKinds returns all error kinds from the error +// this should not be used unless very good reason to do so +func GetAllErrorKinds(err error, defs ...ErrKind) []ErrKind { + kinds := []ErrKind{} + x := &ErrorX{} + parseError(x, err) + if x.kind != nil { + if val, ok := x.kind.(*multiKind); ok { + kinds = append(kinds, val.kinds...) + } else { + kinds = append(kinds, x.kind) + } + } + for _, def := range defs { + if def.Represents(x) { + kinds = append(kinds, def) + } + } + if len(kinds) == 0 { + kinds = append(kinds, ErrKindUnknown) + } + return kinds +} diff --git a/slice/sliceutil_test.go b/slice/sliceutil_test.go index 178540b..712d3a1 100644 --- a/slice/sliceutil_test.go +++ b/slice/sliceutil_test.go @@ -3,6 +3,7 @@ package sliceutil import ( "testing" + osutils "github.com/projectdiscovery/utils/os" "github.com/stretchr/testify/require" ) @@ -287,6 +288,10 @@ func TestVisitRandom(t *testing.T) { } func TestVisitRandomZero(t *testing.T) { + // skipped on windows due to flakiness + if osutils.IsWindows() { + t.Skip("skipping test on windows") + } intSlice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} var timesDifferent int for i := 0; i < 100; i++ {