Add "pretty" json configuration and change default behavior to be space-efficient (#2275)

* expose underlying format options

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove escape html options and address PR feedback

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate PR feedback

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix cli test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2023-11-20 15:29:34 +00:00
committed by GitHub
parent 7cfb5f630a
commit 1676934c63
31 changed files with 723 additions and 204 deletions
+3 -1
View File
@@ -15,6 +15,7 @@ func SupportedVersions() []string {
type EncoderConfig struct {
Version string
Pretty bool // don't include spaces and newlines; same as jq -c
}
type encoder struct {
@@ -23,7 +24,7 @@ type encoder struct {
}
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatJSON)
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatJSON, cfg.Pretty)
if err != nil {
return nil, err
}
@@ -36,6 +37,7 @@ func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
func DefaultEncoderConfig() EncoderConfig {
return EncoderConfig{
Version: cyclonedxutil.DefaultVersion,
Pretty: false,
}
}
+61 -1
View File
@@ -3,12 +3,15 @@ package cyclonedxjson
import (
"bytes"
"flag"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
@@ -16,11 +19,68 @@ var updateSnapshot = flag.Bool("update-cyclonedx-json", false, "update the *.gol
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
func getEncoder(t testing.TB) sbom.FormatEncoder {
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
cfg := DefaultEncoderConfig()
cfg.Pretty = true
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
return enc
}
func TestPrettyOutput(t *testing.T) {
run := func(opt bool) string {
enc, err := NewFormatEncoderWithConfig(EncoderConfig{
Version: cyclonedxutil.DefaultVersion,
Pretty: opt,
})
require.NoError(t, err)
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
return strings.TrimSpace(buffer.String())
}
t.Run("pretty", func(t *testing.T) {
actual := run(true)
assert.Contains(t, actual, "\n")
})
t.Run("compact", func(t *testing.T) {
actual := run(false)
assert.NotContains(t, actual, "\n")
})
}
func TestEscapeHTML(t *testing.T) {
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
s.Artifacts.Packages.Add(pkg.Package{
Name: "<html-package>",
})
// by default we do not escape HTML
t.Run("default", func(t *testing.T) {
cfg := DefaultEncoderConfig()
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
actual := buffer.String()
assert.Contains(t, actual, "<html-package>")
assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
})
}
func TestCycloneDxDirectoryEncoder(t *testing.T) {
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
+3 -1
View File
@@ -17,6 +17,7 @@ func SupportedVersions() []string {
type EncoderConfig struct {
Version string
Pretty bool // set indent to 0
}
type encoder struct {
@@ -25,7 +26,7 @@ type encoder struct {
}
func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatXML)
enc, err := cyclonedxutil.NewEncoder(cfg.Version, cyclonedx.BOMFileFormatXML, cfg.Pretty)
if err != nil {
return nil, err
}
@@ -38,6 +39,7 @@ func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
func DefaultEncoderConfig() EncoderConfig {
return EncoderConfig{
Version: cyclonedxutil.DefaultVersion,
Pretty: false,
}
}
+35 -1
View File
@@ -3,11 +3,14 @@ package cyclonedxxml
import (
"bytes"
"flag"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/format/internal/cyclonedxutil"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/sbom"
)
@@ -16,11 +19,42 @@ var updateSnapshot = flag.Bool("update-cyclonedx-xml", false, "update the *.gold
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
func getEncoder(t testing.TB) sbom.FormatEncoder {
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
cfg := DefaultEncoderConfig()
cfg.Pretty = true
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
return enc
}
func TestPrettyOutput(t *testing.T) {
enc, err := NewFormatEncoderWithConfig(EncoderConfig{
Version: cyclonedxutil.DefaultVersion,
Pretty: false,
})
require.NoError(t, err)
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
actual := buffer.String()
lines := strings.Split(actual, "\n")
require.NotEmpty(t, lines)
whitespace := regexp.MustCompile(`^\s+`)
for _, line := range lines {
if len(line) == 0 {
continue
}
// require a non-whitespace character (tab, space, etc) as the first character of the line
require.False(t, whitespace.Match([]byte(line)), "line should not start with whitespace: %q", line)
}
}
func TestCycloneDxDirectoryEncoder(t *testing.T) {
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
@@ -14,9 +14,10 @@ const DefaultVersion = "1.5"
type Encoder struct {
version cyclonedx.SpecVersion
format cyclonedx.BOMFileFormat
pretty bool
}
func NewEncoder(version string, format cyclonedx.BOMFileFormat) (Encoder, error) {
func NewEncoder(version string, format cyclonedx.BOMFileFormat, pretty bool) (Encoder, error) {
specVersion, err := SpecVersionFromString(version)
if err != nil {
return Encoder{}, err
@@ -24,13 +25,14 @@ func NewEncoder(version string, format cyclonedx.BOMFileFormat) (Encoder, error)
return Encoder{
version: specVersion,
format: format,
pretty: pretty,
}, nil
}
func (e Encoder) Encode(writer io.Writer, s sbom.SBOM) error {
bom := cyclonedxhelpers.ToFormatModel(s)
enc := cyclonedx.NewBOMEncoder(writer, e.format)
enc.SetPretty(true)
enc.SetPretty(e.pretty)
enc.SetEscapeHTML(false)
return enc.EncodeVersion(bom, e.version)
+7 -4
View File
@@ -48,12 +48,8 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon
return
}
var expected []byte
if cfg.Redactor != nil {
actual = cfg.Redactor.Redact(actual)
expected = cfg.Redactor.Redact(testutils.GetGoldenFileContents(t))
} else {
expected = testutils.GetGoldenFileContents(t)
}
if cfg.UpdateSnapshot && cfg.PersistRedactionsInSnapshot {
@@ -62,6 +58,13 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, cfg EncoderSnapshotTestCon
return
}
var expected []byte
if cfg.Redactor != nil {
expected = cfg.Redactor.Redact(testutils.GetGoldenFileContents(t))
} else {
expected = testutils.GetGoldenFileContents(t)
}
if cfg.IsJSON {
require.JSONEq(t, string(expected), string(actual))
} else {
+7 -2
View File
@@ -23,6 +23,7 @@ func SupportedVersions() []string {
type EncoderConfig struct {
Version string
Pretty bool // don't include spaces and newlines; same as jq -c
}
type encoder struct {
@@ -38,6 +39,7 @@ func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
func DefaultEncoderConfig() EncoderConfig {
return EncoderConfig{
Version: spdxutil.DefaultVersion,
Pretty: false,
}
}
@@ -84,9 +86,12 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
}
enc := json.NewEncoder(writer)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if e.cfg.Pretty {
enc.SetIndent("", " ")
}
return enc.Encode(encodeDoc)
}
+61 -1
View File
@@ -3,12 +3,15 @@ package spdxjson
import (
"bytes"
"flag"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/format/internal/spdxutil"
"github.com/anchore/syft/syft/format/internal/testutil"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
@@ -16,11 +19,68 @@ var updateSnapshot = flag.Bool("update-spdx-json", false, "update the *.golden f
var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
func getEncoder(t testing.TB) sbom.FormatEncoder {
enc, err := NewFormatEncoderWithConfig(DefaultEncoderConfig())
cfg := DefaultEncoderConfig()
cfg.Pretty = true
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
return enc
}
func TestPrettyOutput(t *testing.T) {
run := func(opt bool) string {
enc, err := NewFormatEncoderWithConfig(EncoderConfig{
Version: spdxutil.DefaultVersion,
Pretty: opt,
})
require.NoError(t, err)
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
return strings.TrimSpace(buffer.String())
}
t.Run("pretty", func(t *testing.T) {
actual := run(true)
assert.Contains(t, actual, "\n")
})
t.Run("compact", func(t *testing.T) {
actual := run(false)
assert.NotContains(t, actual, "\n")
})
}
func TestEscapeHTML(t *testing.T) {
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
s.Artifacts.Packages.Add(pkg.Package{
Name: "<html-package>",
})
// by default we do not escape HTML
t.Run("default", func(t *testing.T) {
cfg := DefaultEncoderConfig()
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
actual := buffer.String()
assert.Contains(t, actual, "<html-package>")
assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
})
}
func TestSPDXJSONDirectoryEncoder(t *testing.T) {
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
+7 -2
View File
@@ -14,6 +14,7 @@ const ID sbom.FormatID = "syft-json"
type EncoderConfig struct {
Legacy bool // transform the output to the legacy syft-json format (pre v1.0 changes, enumerated in the README.md)
Pretty bool // don't include spaces and newlines; same as jq -c
}
type encoder struct {
@@ -37,6 +38,7 @@ func NewFormatEncoderWithConfig(cfg EncoderConfig) (sbom.FormatEncoder, error) {
func DefaultEncoderConfig() EncoderConfig {
return EncoderConfig{
Legacy: false,
Pretty: false,
}
}
@@ -59,9 +61,12 @@ func (e encoder) Encode(writer io.Writer, s sbom.SBOM) error {
doc := ToFormatModel(s, e.cfg)
enc := json.NewEncoder(writer)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if e.cfg.Pretty {
enc.SetIndent("", " ")
}
return enc.Encode(&doc)
}
+69 -2
View File
@@ -1,9 +1,14 @@
package syftjson
import (
"bytes"
"flag"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
stereoFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
@@ -31,12 +36,69 @@ func TestDefaultNameAndVersion(t *testing.T) {
}
}
func TestPrettyOutput(t *testing.T) {
run := func(opt bool) string {
enc, err := NewFormatEncoderWithConfig(EncoderConfig{
Pretty: opt,
})
require.NoError(t, err)
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
return strings.TrimSpace(buffer.String())
}
t.Run("pretty", func(t *testing.T) {
actual := run(true)
assert.Contains(t, actual, "\n")
})
t.Run("compact", func(t *testing.T) {
actual := run(false)
assert.NotContains(t, actual, "\n")
})
}
func TestEscapeHTML(t *testing.T) {
dir := t.TempDir()
s := testutil.DirectoryInput(t, dir)
s.Artifacts.Packages.Add(pkg.Package{
Name: "<html-package>",
})
// by default we do not escape HTML
t.Run("default", func(t *testing.T) {
cfg := DefaultEncoderConfig()
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
var buffer bytes.Buffer
err = enc.Encode(&buffer, s)
require.NoError(t, err)
actual := buffer.String()
assert.Contains(t, actual, "<html-package>")
assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
})
}
func TestDirectoryEncoder(t *testing.T) {
cfg := DefaultEncoderConfig()
cfg.Pretty = true
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
dir := t.TempDir()
testutil.AssertEncoderAgainstGoldenSnapshot(t,
testutil.EncoderSnapshotTestConfig{
Subject: testutil.DirectoryInput(t, dir),
Format: NewFormatEncoder(),
Format: enc,
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: true,
@@ -46,6 +108,11 @@ func TestDirectoryEncoder(t *testing.T) {
}
func TestImageEncoder(t *testing.T) {
cfg := DefaultEncoderConfig()
cfg.Pretty = true
enc, err := NewFormatEncoderWithConfig(cfg)
require.NoError(t, err)
testImage := "image-simple"
testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
testutil.ImageSnapshotTestConfig{
@@ -54,7 +121,7 @@ func TestImageEncoder(t *testing.T) {
},
testutil.EncoderSnapshotTestConfig{
Subject: testutil.ImageInput(t, testImage, testutil.FromSnapshot()),
Format: NewFormatEncoder(),
Format: enc,
UpdateSnapshot: *updateSnapshot,
PersistRedactionsInSnapshot: true,
IsJSON: true,