Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions pkg/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
Expand All @@ -15,10 +16,12 @@ func LoadArtifactFromFile(filename string, mediaType string) (*ocispec.Descripto
return nil, nil, fmt.Errorf("error loading artifact from file: %w", err)
}

return LoadArtifactFromReader(file, mediaType)
// Extract just the filename without the path for the annotation
baseFilename := filepath.Base(filename)
return LoadArtifactFromReader(file, mediaType, baseFilename)
}

func LoadArtifactFromReader(reader io.ReadCloser, mediaType string) (*ocispec.Descriptor, []byte, error) {
func LoadArtifactFromReader(reader io.ReadCloser, mediaType string, filename ...string) (*ocispec.Descriptor, []byte, error) {
defer reader.Close()

// Read all the bytes from the reader into a slice
Expand All @@ -29,5 +32,13 @@ func LoadArtifactFromReader(reader io.ReadCloser, mediaType string) (*ocispec.De

desc := content.NewDescriptorFromBytes(mediaType, artifactBytes)

// Add artifact filename annotation if filename is provided and not empty
if len(filename) > 0 && filename[0] != "" {
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
desc.Annotations[ocispec.AnnotationTitle] = filename[0]
}

return &desc, artifactBytes, nil
}
243 changes: 243 additions & 0 deletions pkg/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package obom
import (
"bytes"
"io"
"os"
"strings"
"testing"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestLoadArtifactFromFile(t *testing.T) {
Expand Down Expand Up @@ -60,3 +64,242 @@ func TestLoadArtifactFromReader(t *testing.T) {
t.Errorf("expected desc.Digest to be 'sha256:40b61fe1b15af0a4d5402735b26343e8cf8a045f4d81710e6108a21d91eaf366', got: %s", desc.Digest.String())
}
}

func TestLoadArtifactFromReader_WithFilename(t *testing.T) {
// Define the test data and its size
testData := []byte(`{"test": "data"}`)
mediaType := "application/json"
artifactFilename := "test-artifact.json"

// Create a test reader with the test data
reader := io.NopCloser(bytes.NewReader(testData))

// Call the function with the test reader, media type, and filename
desc, _, err := LoadArtifactFromReader(reader, mediaType, artifactFilename)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

// Check that the artifact filename annotation is set
if desc.Annotations == nil {
t.Fatalf("expected annotations to be set, got nil")
}

title, exists := desc.Annotations[ocispec.AnnotationTitle]
if !exists {
t.Errorf("expected annotation %s to exist", ocispec.AnnotationTitle)
}
if title != artifactFilename {
t.Errorf("expected annotation %s to be '%s', got: %s", ocispec.AnnotationTitle, artifactFilename, title)
}
}

func TestLoadArtifactFromReader_WithoutFilename(t *testing.T) {
// Define the test data and its size
testData := []byte(`{"test": "data"}`)
mediaType := "application/json"

// Create a test reader with the test data
reader := io.NopCloser(bytes.NewReader(testData))

// Call the function with the test reader and media type, but no filename
desc, _, err := LoadArtifactFromReader(reader, mediaType)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

// Check that no annotations are set when filename is empty
if desc.Annotations != nil {
if _, exists := desc.Annotations[ocispec.AnnotationTitle]; exists {
t.Errorf("expected no %s annotation when filename is empty, but it was set", ocispec.AnnotationTitle)
}
}
}

func TestLoadArtifactFromReader_BackwardCompatibility(t *testing.T) {
// Test that calling without the filename parameter still works (backward compatibility)
testData := []byte(`{"test": "data"}`)
mediaType := "application/json"

// Create a test reader with the test data
reader := io.NopCloser(bytes.NewReader(testData))

// Call the function with just the required parameters (old way)
desc, _, err := LoadArtifactFromReader(reader, mediaType)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

// Check that no annotations are set when filename is not provided
if desc.Annotations != nil {
if _, exists := desc.Annotations[ocispec.AnnotationTitle]; exists {
t.Errorf("expected no %s annotation when filename is not provided, but it was set", ocispec.AnnotationTitle)
}
}
}

func TestLoadSBOMFromReader_BackwardCompatibility(t *testing.T) {
// Test that calling without the filename parameter still works (backward compatibility)
spdxStr := `{
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.2",
"name" : "SPDX-Example",
"documentNamespace" : "SPDX-Namespace-Example",
"creationInfo": {
"created": "2020-07-23T18:30:22Z",
"creators": ["Tool: SPDX-Java-Tools-v2.1.20"]
}
}`

// Create a test reader with the SPDX JSON data
reader := io.NopCloser(strings.NewReader(spdxStr))

// Call the function with just the required parameters (old way)
sbomDoc, desc, _, err := LoadSBOMFromReader(reader, true)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

// Check that the SBOM document was loaded correctly
if sbomDoc.Version != "SPDX-2.2" {
t.Errorf("expected SPDXVersion to be 'SPDX-2.2', got: %v", sbomDoc.Version)
}

// Check that no filename annotation is set when filename is not provided
if desc.Annotations != nil {
if _, exists := desc.Annotations[ocispec.AnnotationTitle]; exists {
t.Errorf("expected no %s annotation when filename is not provided, but it was set", ocispec.AnnotationTitle)
}
}
}

func TestLoadSBOMFromReader_WithOptionalFilename(t *testing.T) {
// Test that calling with the optional filename parameter works
spdxStr := `{
"SPDXID": "SPDXRef-DOCUMENT",
"spdxVersion": "SPDX-2.2",
"name" : "SPDX-Example",
"documentNamespace" : "SPDX-Namespace-Example",
"creationInfo": {
"created": "2020-07-23T18:30:22Z",
"creators": ["Tool: SPDX-Java-Tools-v2.1.20"]
}
}`

// Create a test reader with the SPDX JSON data
reader := io.NopCloser(strings.NewReader(spdxStr))
artifactFilename := "test-sbom.spdx.json"

// Call the function with the optional filename parameter
sbomDoc, desc, _, err := LoadSBOMFromReader(reader, true, artifactFilename)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

// Check that the SBOM document was loaded correctly
if sbomDoc.Version != "SPDX-2.2" {
t.Errorf("expected SPDXVersion to be 'SPDX-2.2', got: %v", sbomDoc.Version)
}

// Check that the artifact filename annotation is set
if desc.Annotations == nil {
t.Fatalf("expected annotations to be set, got nil")
}

title, exists := desc.Annotations[ocispec.AnnotationTitle]
if !exists {
t.Errorf("expected annotation %s to exist", ocispec.AnnotationTitle)
}
if title != artifactFilename {
t.Errorf("expected annotation %s to be '%s', got: %s", ocispec.AnnotationTitle, artifactFilename, title)
}
}

func TestLoadArtifactFromFile_ArtifactFilenameAnnotation(t *testing.T) {
// Define the path to the test file
filePath := "../examples/artifact.example.json"
mediaType := "application/json"
expectedFilename := "artifact.example.json" // Only the base filename, not the full path

// Call the function with the test file path and media type
desc, _, err := LoadArtifactFromFile(filePath, mediaType)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}

// Check that the artifact filename annotation is set with just the filename
if desc.Annotations == nil {
t.Fatalf("expected annotations to be set, got nil")
}

title, exists := desc.Annotations[ocispec.AnnotationTitle]
if !exists {
t.Errorf("expected annotation %s to exist", ocispec.AnnotationTitle)
}
if title != expectedFilename {
t.Errorf("expected annotation %s to be '%s', got: %s", ocispec.AnnotationTitle, expectedFilename, title)
}
}

func TestLoadArtifactFromFile_FilenameExtractionFromPath(t *testing.T) {
// Test that filename is correctly extracted from various path formats
testCases := []struct {
name string
filePath string
expectedName string
}{
{
name: "relative path with slash",
filePath: "../examples/artifact.example.json",
expectedName: "artifact.example.json",
},
{
name: "simple filename",
filePath: "artifact.example.json",
expectedName: "artifact.example.json",
},
}

mediaType := "application/json"

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Skip if file doesn't exist (we're testing the filename extraction logic)
if _, err := os.Stat(tc.filePath); os.IsNotExist(err) {
t.Skipf("Test file %s does not exist", tc.filePath)
}

desc, _, err := LoadArtifactFromFile(tc.filePath, mediaType)

// Check that there was no error
if err != nil {
t.Fatalf("expected no error for path '%s', got: %v", tc.filePath, err)
}

// Check that only the filename (not the path) is in the annotation
if desc.Annotations == nil {
t.Fatalf("expected annotations to be set for path '%s', got nil", tc.filePath)
}

title, exists := desc.Annotations[ocispec.AnnotationTitle]
if !exists {
t.Errorf("expected annotation %s to exist for path '%s'", ocispec.AnnotationTitle, tc.filePath)
}
if title != tc.expectedName {
t.Errorf("for path '%s', expected annotation %s to be '%s', got: %s", tc.filePath, ocispec.AnnotationTitle, tc.expectedName, title)
}
})
}
}
11 changes: 8 additions & 3 deletions pkg/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
Expand Down Expand Up @@ -33,23 +34,27 @@ type SPDXDocument struct {
// LoadSBOMFromFile opens a file given by filename, reads its contents, and loads it into an SPDX document.
// It also calculates the file size and generates an OCI descriptor for the file.
// It returns the loaded SPDX document, the OCI descriptor, and any error encountered.
// The filename path is used to open the file, but only the base filename is used for annotations.
func LoadSBOMFromFile(filename string, strict bool) (*SPDXDocument, *ocispec.Descriptor, []byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, nil, nil, err
}
defer file.Close()

return LoadSBOMFromReader(file, strict)
// Extract just the filename without the path for the annotation
baseFilename := filepath.Base(filename)
return LoadSBOMFromReader(file, strict, baseFilename)
}

// LoadSBOMFromReader reads an SPDX document from an io.ReadCloser, generates an OCI descriptor for the document,
// and returns the loaded SPDX document and the OCI descriptor.
// If an error occurs during reading the document or generating the descriptor, the error will be returned.
func LoadSBOMFromReader(reader io.ReadCloser, strict bool) (*SPDXDocument, *ocispec.Descriptor, []byte, error) {
// The filename parameter is optional - if provided, it will be added as an annotation to the descriptor.
func LoadSBOMFromReader(reader io.ReadCloser, strict bool, filename ...string) (*SPDXDocument, *ocispec.Descriptor, []byte, error) {
defer reader.Close()

desc, sbomBytes, err := LoadArtifactFromReader(reader, MEDIATYPE_SPDX)
desc, sbomBytes, err := LoadArtifactFromReader(reader, MEDIATYPE_SPDX, filename...)
if err != nil {
return nil, nil, nil, err
}
Expand Down
Loading