Skip to content
Merged
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
30 changes: 29 additions & 1 deletion pkg/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"

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

return LoadArtifactFromReader(file, mediaType)
desc, artifactBytes, err := LoadArtifactFromReader(file, mediaType)
if err != nil {
return nil, nil, err
}

// Add filename annotation if missing
AddFilenameAnnotationIfMissing(desc, filename)

return desc, artifactBytes, nil
}

func LoadArtifactFromReader(reader io.ReadCloser, mediaType string) (*ocispec.Descriptor, []byte, error) {
Expand All @@ -31,3 +41,21 @@ func LoadArtifactFromReader(reader io.ReadCloser, mediaType string) (*ocispec.De

return &desc, artifactBytes, nil
}

// AddFilenameAnnotationIfMissing adds a title annotation to the descriptor using the base filename
// if the annotation doesn't already exist or is empty. This function modifies the descriptor in-place.
func AddFilenameAnnotationIfMissing(desc *ocispec.Descriptor, filename string) {
if desc.Annotations == nil || desc.Annotations[ocispec.AnnotationTitle] == "" {
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
// Use only the base filename, not the full path
// Handle both Unix and Windows path separators regardless of platform
basename := filepath.Base(filename)
// If filepath.Base didn't extract properly (e.g., Windows paths on Unix), try manual extraction
if lastSlash := strings.LastIndexByte(basename, '\\'); lastSlash >= 0 {
basename = basename[lastSlash+1:]
}
desc.Annotations[ocispec.AnnotationTitle] = basename
}
}
258 changes: 258 additions & 0 deletions pkg/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"io"
"testing"

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

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

func TestLoadArtifactFromFile_AddsFilenameAnnotation(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 TestLoadArtifactFromReader_NoAnnotations(t *testing.T) {
// Test that LoadArtifactFromReader doesn't add annotations
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
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 since LoadArtifactFromReader doesn't add title annotations
if desc.Annotations != nil {
if _, exists := desc.Annotations[ocispec.AnnotationTitle]; exists {
t.Errorf("expected no %s annotation from LoadArtifactFromReader, but it was set", ocispec.AnnotationTitle)
}
}
}

func TestLoadArtifactFromFile_FilenameExtractionFromPath(t *testing.T) {
// Test that the filename is correctly extracted from various path formats
// Since we can't easily test different paths to the same file, we just verify
// that the existing test file produces the expected filename
filePath := "../examples/artifact.example.json"
expectedFilename := "artifact.example.json"
mediaType := "application/json"

// Call the function
desc, _, err := LoadArtifactFromFile(filePath, mediaType)

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

// Verify that only the filename (not the path) is in the annotation
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 TestAddFilenameAnnotationIfMissing_AddsAnnotationWhenMissing(t *testing.T) {
// Test adding annotation when descriptor has no annotations
desc := &ocispec.Descriptor{
MediaType: "application/json",
Size: 100,
Digest: "sha256:abc123",
}
filename := "../path/to/test-file.json"
expectedFilename := "test-file.json"

AddFilenameAnnotationIfMissing(desc, filename)

// Check that annotations were created and title was set
if desc.Annotations == nil {
t.Fatalf("expected annotations to be created, 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 TestAddFilenameAnnotationIfMissing_AddsAnnotationWhenEmpty(t *testing.T) {
// Test adding annotation when descriptor has empty annotations map
desc := &ocispec.Descriptor{
MediaType: "application/json",
Size: 100,
Digest: "sha256:abc123",
Annotations: make(map[string]string),
}
filename := "test-file.json"
expectedFilename := "test-file.json"

AddFilenameAnnotationIfMissing(desc, filename)

// Check that title annotation was added
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 TestAddFilenameAnnotationIfMissing_AddsAnnotationWhenTitleEmpty(t *testing.T) {
// Test adding annotation when descriptor has annotations but empty title
desc := &ocispec.Descriptor{
MediaType: "application/json",
Size: 100,
Digest: "sha256:abc123",
Annotations: map[string]string{
"other.annotation": "some-value",
ocispec.AnnotationTitle: "", // Empty title
},
}
filename := "C:\\Windows\\System32\\test-file.json"
expectedFilename := "test-file.json"

AddFilenameAnnotationIfMissing(desc, filename)

// Check that title annotation was set
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)
}

// Check that other annotations are preserved
other, exists := desc.Annotations["other.annotation"]
if !exists || other != "some-value" {
t.Errorf("expected other annotations to be preserved")
}
}

func TestAddFilenameAnnotationIfMissing_PreservesExistingAnnotation(t *testing.T) {
// Test that existing non-empty title annotation is preserved
existingTitle := "existing-title.json"
desc := &ocispec.Descriptor{
MediaType: "application/json",
Size: 100,
Digest: "sha256:abc123",
Annotations: map[string]string{
ocispec.AnnotationTitle: existingTitle,
"other.annotation": "some-value",
},
}
filename := "new-filename.json"

AddFilenameAnnotationIfMissing(desc, filename)

// Check that existing title annotation was preserved
title, exists := desc.Annotations[ocispec.AnnotationTitle]
if !exists {
t.Errorf("expected annotation %s to exist", ocispec.AnnotationTitle)
}
if title != existingTitle {
t.Errorf("expected annotation %s to be preserved as '%s', got: %s", ocispec.AnnotationTitle, existingTitle, title)
}

// Check that other annotations are preserved
other, exists := desc.Annotations["other.annotation"]
if !exists || other != "some-value" {
t.Errorf("expected other annotations to be preserved")
}
}

func TestAddFilenameAnnotationIfMissing_HandlesVariousPathFormats(t *testing.T) {
// Test that the function correctly extracts filenames from various path formats
testCases := []struct {
name string
inputPath string
expectedName string
}{
{
name: "relative path with slash",
inputPath: "../examples/test.json",
expectedName: "test.json",
},
{
name: "simple filename",
inputPath: "test.json",
expectedName: "test.json",
},
{
name: "absolute unix path",
inputPath: "/path/to/test.json",
expectedName: "test.json",
},
{
name: "absolute windows path",
inputPath: "C:\\Windows\\System32\\test.json",
expectedName: "test.json",
},
{
name: "current directory",
inputPath: "./test.json",
expectedName: "test.json",
},
{
name: "windows style backslash",
inputPath: "examples\\test.json",
expectedName: "test.json",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
desc := &ocispec.Descriptor{
MediaType: "application/json",
Size: 100,
Digest: "sha256:abc123",
}

AddFilenameAnnotationIfMissing(desc, tc.inputPath)

// Check that the correct filename was extracted
title, exists := desc.Annotations[ocispec.AnnotationTitle]
if !exists {
t.Errorf("expected annotation %s to exist", ocispec.AnnotationTitle)
}
if title != tc.expectedName {
t.Errorf("for path '%s', expected annotation %s to be '%s', got: %s", tc.inputPath, ocispec.AnnotationTitle, tc.expectedName, title)
}
})
}
}
11 changes: 10 additions & 1 deletion pkg/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,23 @@ 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.
// If the descriptor doesn't have a title annotation, it will be added using the base filename.
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)
doc, desc, sbomBytes, err := LoadSBOMFromReader(file, strict)
if err != nil {
return nil, nil, nil, err
}

// Add filename annotation if missing
AddFilenameAnnotationIfMissing(desc, filename)

return doc, desc, sbomBytes, nil
}

// LoadSBOMFromReader reads an SPDX document from an io.ReadCloser, generates an OCI descriptor for the document,
Expand Down
Loading
Loading