github.com/anchore/syft@v1.38.2/internal/unknown/coordinate_error.go (about) 1 package unknown 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 8 "github.com/anchore/syft/internal/log" 9 "github.com/anchore/syft/syft/file" 10 ) 11 12 type hasCoordinates interface { 13 GetCoordinates() file.Coordinates 14 } 15 16 type CoordinateError struct { 17 Coordinates file.Coordinates 18 Reason error 19 } 20 21 var _ error = (*CoordinateError)(nil) 22 23 func (u *CoordinateError) Error() string { 24 if u.Coordinates.FileSystemID == "" { 25 return fmt.Sprintf("%s: %v", u.Coordinates.RealPath, u.Reason) 26 } 27 return fmt.Sprintf("%s (%s): %v", u.Coordinates.RealPath, u.Coordinates.FileSystemID, u.Reason) 28 } 29 30 // New returns a new CoordinateError unless the reason is a CoordinateError itself, in which case 31 // reason will be returned directly or if reason is nil, nil will be returned 32 func New(coords hasCoordinates, reason error) *CoordinateError { 33 if reason == nil { 34 return nil 35 } 36 coordinates := coords.GetCoordinates() 37 reasonCoordinateError := &CoordinateError{} 38 if errors.As(reason, &reasonCoordinateError) { 39 // if the reason is already a coordinate error, it is potentially for a different location, 40 // so we do not want to surface this location having an error 41 return reasonCoordinateError 42 } 43 return &CoordinateError{ 44 Coordinates: coordinates, 45 Reason: reason, 46 } 47 } 48 49 // Newf returns a new CoordinateError with a reason of an error created from given format and args 50 func Newf(coords hasCoordinates, format string, args ...any) *CoordinateError { 51 return New(coords, fmt.Errorf(format, args...)) 52 } 53 54 // Append returns an error joined to the first error/set of errors, with a new CoordinateError appended to the end 55 func Append(errs error, coords hasCoordinates, reason error) error { 56 return Join(errs, New(coords, reason)) 57 } 58 59 // Appendf returns an error joined to the first error/set of errors, with a new CoordinateError appended to the end, 60 // created from the given reason and args 61 func Appendf(errs error, coords hasCoordinates, format string, args ...any) error { 62 return Append(errs, coords, fmt.Errorf(format, args...)) 63 } 64 65 // Join joins the provided sets of errors together in a flattened manner, taking into account nested errors created 66 // from other sources, including errors.Join, multierror.Append, and unknown.Join 67 func Join(errs ...error) error { 68 var out []error 69 for _, err := range errs { 70 // append errors, de-duplicated 71 for _, e := range flatten(err) { 72 if containsErr(out, e) { 73 continue 74 } 75 out = append(out, e) 76 } 77 } 78 if len(out) == 1 { 79 return out[0] 80 } 81 if len(out) == 0 { 82 return nil 83 } 84 return errors.Join(out...) 85 } 86 87 // Joinf joins the provided sets of errors together in a flattened manner, taking into account nested errors created 88 // from other sources, including errors.Join, multierror.Append, and unknown.Join and appending a new error, 89 // created from the format and args provided -- the error is NOT a CoordinateError 90 func Joinf(errs error, format string, args ...any) error { 91 return Join(errs, fmt.Errorf(format, args...)) 92 } 93 94 // IfEmptyf returns a new Errorf-formatted error, only when the provided slice is empty or nil when 95 // the slice has entries 96 func IfEmptyf[T any](emptyTest []T, format string, args ...any) error { 97 if len(emptyTest) == 0 { 98 return fmt.Errorf(format, args...) 99 } 100 return nil 101 } 102 103 // ExtractCoordinateErrors extracts all coordinate errors returned, and any _additional_ errors in the graph 104 // are encapsulated in the second, error return parameter 105 func ExtractCoordinateErrors(err error) (coordinateErrors []CoordinateError, remainingErrors error) { 106 remainingErrors = visitErrors(err, func(e error) error { 107 if coordinateError, _ := e.(*CoordinateError); coordinateError != nil { 108 coordinateErrors = append(coordinateErrors, *coordinateError) 109 return nil 110 } 111 return e 112 }) 113 return coordinateErrors, remainingErrors 114 } 115 116 func flatten(errs ...error) []error { 117 var out []error 118 for _, err := range errs { 119 if err == nil { 120 continue 121 } 122 // turn all errors nested under a coordinate error to individual coordinate errors 123 if e, ok := err.(*CoordinateError); ok { 124 if e == nil { 125 continue 126 } 127 for _, r := range flatten(e.Reason) { 128 out = append(out, New(e.Coordinates, r)) 129 } 130 } else 131 // from multierror.Append 132 if e, ok := err.(interface{ WrappedErrors() []error }); ok { 133 if e == nil { 134 continue 135 } 136 out = append(out, flatten(e.WrappedErrors()...)...) 137 } else 138 // from errors.Join 139 if e, ok := err.(interface{ Unwrap() []error }); ok { 140 if e == nil { 141 continue 142 } 143 out = append(out, flatten(e.Unwrap()...)...) 144 } else { 145 out = append(out, err) 146 } 147 } 148 return out 149 } 150 151 // containsErr returns true if a duplicate error is found 152 func containsErr(out []error, err error) bool { 153 defer func() { 154 if err := recover(); err != nil { 155 log.Tracef("error comparing errors: %v", err) 156 } 157 }() 158 for _, e := range out { 159 if e == err { 160 return true 161 } 162 } 163 return false 164 } 165 166 // visitErrors visits every wrapped error. the returned error replaces the provided error, null errors are omitted from 167 // the object graph 168 func visitErrors(err error, fn func(error) error) error { 169 // unwrap errors from errors.Join 170 if errs, ok := err.(interface{ Unwrap() []error }); ok { 171 var out []error 172 for _, e := range errs.Unwrap() { 173 out = append(out, visitErrors(e, fn)) 174 } 175 // errors.Join omits nil errors and will return nil if all passed errors are nil 176 return errors.Join(out...) 177 } 178 // unwrap errors from multierror.Append -- these also implement Unwrap() error, so check this first 179 if errs, ok := err.(interface{ WrappedErrors() []error }); ok { 180 var out []error 181 for _, e := range errs.WrappedErrors() { 182 out = append(out, visitErrors(e, fn)) 183 } 184 // errors.Join omits nil errors and will return nil if all passed errors are nil 185 return errors.Join(out...) 186 } 187 // unwrap singly wrapped errors 188 if e, ok := err.(interface{ Unwrap() error }); ok { 189 wrapped := e.Unwrap() 190 got := visitErrors(wrapped, fn) 191 if got == nil { 192 return nil 193 } 194 if wrapped.Error() != got.Error() { 195 prefix := strings.TrimSuffix(err.Error(), wrapped.Error()) 196 return fmt.Errorf("%s%w", prefix, got) 197 } 198 return err 199 } 200 return fn(err) 201 }