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  }