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 }