github.com/anchore/syft@v1.38.2/syft/format/table/encoder.go (about)

     1  package table
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/charmbracelet/lipgloss"
    10  	"github.com/olekukonko/tablewriter"
    11  	"github.com/olekukonko/tablewriter/renderer"
    12  	"github.com/olekukonko/tablewriter/tw"
    13  
    14  	"github.com/anchore/syft/syft/sbom"
    15  )
    16  
    17  const ID sbom.FormatID = "syft-table"
    18  
    19  type encoder struct {
    20  }
    21  
    22  func NewFormatEncoder() sbom.FormatEncoder {
    23  	return encoder{}
    24  }
    25  
    26  func (e encoder) ID() sbom.FormatID {
    27  	return ID
    28  }
    29  
    30  func (e encoder) Aliases() []string {
    31  	return []string{
    32  		"table",
    33  	}
    34  }
    35  
    36  func (e encoder) Version() string {
    37  	return sbom.AnyVersion
    38  }
    39  
    40  func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
    41  	var rows [][]string
    42  
    43  	columns := []string{"Name", "Version", "Type"}
    44  	for _, p := range s.Artifacts.Packages.Sorted() {
    45  		row := []string{
    46  			p.Name,
    47  			p.Version,
    48  			string(p.Type),
    49  		}
    50  		rows = append(rows, row)
    51  	}
    52  
    53  	if len(rows) == 0 {
    54  		_, err := fmt.Fprintln(writer, "No packages discovered")
    55  		return err
    56  	}
    57  
    58  	// sort by name, version, then type
    59  	sort.SliceStable(rows, func(i, j int) bool {
    60  		for col := 0; col < len(columns); col++ {
    61  			if rows[i][col] != rows[j][col] {
    62  				return rows[i][col] < rows[j][col]
    63  			}
    64  		}
    65  		return false
    66  	})
    67  
    68  	columns = append(columns, "") // add a column for duplicate annotations
    69  	rows = markDuplicateRows(rows)
    70  
    71  	table := newTableWriter(writer, columns)
    72  
    73  	if err := table.Bulk(rows); err != nil {
    74  		return fmt.Errorf("failed to add table rows: %w", err)
    75  	}
    76  
    77  	return table.Render()
    78  }
    79  
    80  func newTableWriter(writer io.Writer, columns []string) *tablewriter.Table {
    81  	// Here’s a simplified diagram of a table with a header, rows, and footer:
    82  	//
    83  	// [Borders.Top]
    84  	// | Header1 | Header2 |  (Line below header: Lines.ShowTop)
    85  	// [Separators.BetweenRows]
    86  	// | Row1    | Row1    |
    87  	// [Separators.BetweenRows]
    88  	// | Row2    | Row2    |
    89  	// [Lines.ShowBottom]
    90  	// | Footer1 | Footer2 |
    91  	// [Borders.Bottom]
    92  	//
    93  	// So for example:
    94  	// ┌──────┬─────┐  <- Borders.Top
    95  	// │ NAME │ AGE │
    96  	// ├──────┼─────┤  <- Lines.ShowTop
    97  	// │ Alice│ 25  │
    98  	// ├──────┼─────┤  <- Separators.BetweenRows
    99  	// │ Bob  │ 30  │
   100  	// ├──────┼─────┤  <- Lines.ShowBottom
   101  	// │ Total│ 2   │
   102  	// └──────┴─────┘  <- Borders.Bottom
   103  
   104  	return tablewriter.NewTable(writer,
   105  		tablewriter.WithHeader(columns),
   106  		tablewriter.WithHeaderAutoFormat(tw.On),
   107  		tablewriter.WithHeaderAutoWrap(tw.WrapNone),
   108  		tablewriter.WithHeaderAlignment(tw.AlignLeft),
   109  		tablewriter.WithRowAutoFormat(tw.Off),
   110  		tablewriter.WithRowAutoWrap(tw.WrapNone),
   111  		tablewriter.WithRowAlignment(tw.AlignLeft),
   112  		tablewriter.WithTrimSpace(tw.On),
   113  		tablewriter.WithAutoHide(tw.On),
   114  		tablewriter.WithRenderer(renderer.NewBlueprint()),
   115  		tablewriter.WithBehavior(
   116  			tw.Behavior{
   117  				TrimSpace: tw.On,
   118  				AutoHide:  tw.On,
   119  			},
   120  		),
   121  		tablewriter.WithPadding(
   122  			tw.Padding{
   123  				Left:   "",
   124  				Right:  "  ",
   125  				Top:    "",
   126  				Bottom: "",
   127  			},
   128  		),
   129  		tablewriter.WithRendition(
   130  			tw.Rendition{
   131  				Symbols: tw.NewSymbols(tw.StyleNone),
   132  				Borders: tw.Border{
   133  					Left:   tw.Off,
   134  					Top:    tw.Off,
   135  					Right:  tw.Off,
   136  					Bottom: tw.Off,
   137  				},
   138  				Settings: tw.Settings{
   139  					Separators: tw.Separators{
   140  						ShowHeader:     tw.Off,
   141  						ShowFooter:     tw.Off,
   142  						BetweenRows:    tw.Off,
   143  						BetweenColumns: tw.Off,
   144  					},
   145  					Lines: tw.Lines{
   146  						ShowTop:        tw.Off,
   147  						ShowBottom:     tw.Off,
   148  						ShowHeaderLine: tw.Off,
   149  						ShowFooterLine: tw.Off,
   150  					},
   151  				},
   152  			},
   153  		),
   154  	)
   155  }
   156  
   157  func markDuplicateRows(items [][]string) [][]string {
   158  	seen := map[string]int{}
   159  	var result [][]string
   160  
   161  	for _, v := range items {
   162  		key := strings.Join(v, "|")
   163  		if _, ok := seen[key]; ok {
   164  			// dup!
   165  			seen[key]++
   166  			continue
   167  		}
   168  
   169  		seen[key] = 1
   170  		result = append(result, v)
   171  	}
   172  
   173  	style := lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
   174  	for i, v := range result {
   175  		key := strings.Join(v, "|")
   176  		// var name string
   177  		var annotation string
   178  		switch seen[key] {
   179  		case 0, 1:
   180  		case 2:
   181  			annotation = "(+1 duplicate)"
   182  		default:
   183  			annotation = fmt.Sprintf("(+%d duplicates)", seen[key]-1)
   184  		}
   185  
   186  		annotation = style.Render(annotation)
   187  		result[i] = append(v, annotation)
   188  	}
   189  
   190  	return result
   191  }