github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/cmd/gosbom/cli/options/writer.go (about)

     1  package options
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/go-multierror"
    11  	"github.com/mitchellh/go-homedir"
    12  	"github.com/nextlinux/gosbom/gosbom/formats"
    13  	"github.com/nextlinux/gosbom/gosbom/formats/table"
    14  	"github.com/nextlinux/gosbom/gosbom/formats/template"
    15  	"github.com/nextlinux/gosbom/gosbom/sbom"
    16  	"github.com/nextlinux/gosbom/internal/log"
    17  )
    18  
    19  var _ sbom.Writer = (*sbomMultiWriter)(nil)
    20  
    21  var _ interface {
    22  	io.Closer
    23  	sbom.Writer
    24  } = (*sbomStreamWriter)(nil)
    25  
    26  // MakeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer
    27  // or an error but neither both and if there is no error, sbom.Writer.Close() should be called
    28  func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) {
    29  	outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  
    34  	writer, err := newSBOMMultiWriter(outputOptions...)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	return writer, nil
    40  }
    41  
    42  // MakeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error.
    43  func MakeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) {
    44  	writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path))
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	return writer, nil
    50  }
    51  
    52  // parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
    53  func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string) (out []sbomWriterDescription, errs error) {
    54  	// always should have one option -- we generally get the default of "table", but just make sure
    55  	if len(outputs) == 0 {
    56  		outputs = append(outputs, table.ID.String())
    57  	}
    58  
    59  	for _, name := range outputs {
    60  		name = strings.TrimSpace(name)
    61  
    62  		// split to at most two parts for <format>=<file>
    63  		parts := strings.SplitN(name, "=", 2)
    64  
    65  		// the format name is the first part
    66  		name = parts[0]
    67  
    68  		// default to the --file or empty string if not specified
    69  		file := defaultFile
    70  
    71  		// If a file is specified as part of the output formatName, use that
    72  		if len(parts) > 1 {
    73  			file = parts[1]
    74  		}
    75  
    76  		format := formats.ByName(name)
    77  		if format == nil {
    78  			errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, formats.AllIDs()))
    79  			continue
    80  		}
    81  
    82  		if tmpl, ok := format.(template.OutputFormat); ok {
    83  			tmpl.SetTemplatePath(templateFilePath)
    84  			format = tmpl
    85  		}
    86  
    87  		out = append(out, newSBOMWriterDescription(format, file))
    88  	}
    89  	return out, errs
    90  }
    91  
    92  // sbomWriterDescription Format and path strings used to create sbom.Writer
    93  type sbomWriterDescription struct {
    94  	Format sbom.Format
    95  	Path   string
    96  }
    97  
    98  func newSBOMWriterDescription(f sbom.Format, p string) sbomWriterDescription {
    99  	expandedPath, err := homedir.Expand(p)
   100  	if err != nil {
   101  		log.Warnf("could not expand given writer output path=%q: %w", p, err)
   102  		// ignore errors
   103  		expandedPath = p
   104  	}
   105  	return sbomWriterDescription{
   106  		Format: f,
   107  		Path:   expandedPath,
   108  	}
   109  }
   110  
   111  // sbomMultiWriter holds a list of child sbom.Writers to apply all Write and Close operations to
   112  type sbomMultiWriter struct {
   113  	writers []sbom.Writer
   114  }
   115  
   116  type nopWriteCloser struct {
   117  	io.Writer
   118  }
   119  
   120  func (n nopWriteCloser) Close() error {
   121  	return nil
   122  }
   123  
   124  // newSBOMMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used
   125  func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) {
   126  	if len(options) == 0 {
   127  		return nil, fmt.Errorf("no output options provided")
   128  	}
   129  
   130  	out := &sbomMultiWriter{}
   131  
   132  	for _, option := range options {
   133  		switch len(option.Path) {
   134  		case 0:
   135  			out.writers = append(out.writers, &sbomStreamWriter{
   136  				format: option.Format,
   137  				out:    nopWriteCloser{Writer: os.Stdout},
   138  			})
   139  		default:
   140  			// create any missing subdirectories
   141  			dir := path.Dir(option.Path)
   142  			if dir != "" {
   143  				s, err := os.Stat(dir)
   144  				if err != nil {
   145  					err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
   146  					if err != nil {
   147  						return nil, err
   148  					}
   149  				} else if !s.IsDir() {
   150  					return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
   151  				}
   152  			}
   153  			fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
   154  			if err != nil {
   155  				return nil, fmt.Errorf("unable to create report file: %w", err)
   156  			}
   157  			out.writers = append(out.writers, &sbomStreamWriter{
   158  				format: option.Format,
   159  				out:    fileOut,
   160  			})
   161  		}
   162  	}
   163  
   164  	return out, nil
   165  }
   166  
   167  // Write writes the SBOM to all writers
   168  func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) {
   169  	for _, w := range m.writers {
   170  		err := w.Write(s)
   171  		if err != nil {
   172  			errs = multierror.Append(errs, fmt.Errorf("unable to write SBOM: %w", err))
   173  		}
   174  	}
   175  	return errs
   176  }
   177  
   178  // sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup
   179  type sbomStreamWriter struct {
   180  	format sbom.Format
   181  	out    io.Writer
   182  }
   183  
   184  // Write the provided SBOM to the data stream
   185  func (w *sbomStreamWriter) Write(s sbom.SBOM) error {
   186  	defer w.Close()
   187  	return w.format.Encode(w.out, s)
   188  }
   189  
   190  // Close any resources, such as open files
   191  func (w *sbomStreamWriter) Close() error {
   192  	if closer, ok := w.out.(io.Closer); ok {
   193  		return closer.Close()
   194  	}
   195  	return nil
   196  }