aboutsummaryrefslogtreecommitdiff
path: root/tvsaver/saver2v3
diff options
context:
space:
mode:
Diffstat (limited to 'tvsaver/saver2v3')
-rw-r--r--tvsaver/saver2v3/save_annotation.go32
-rw-r--r--tvsaver/saver2v3/save_annotation_test.go110
-rw-r--r--tvsaver/saver2v3/save_creation_info.go30
-rw-r--r--tvsaver/saver2v3/save_creation_info_test.go112
-rw-r--r--tvsaver/saver2v3/save_document.go104
-rw-r--r--tvsaver/saver2v3/save_document_test.go343
-rw-r--r--tvsaver/saver2v3/save_file.go81
-rw-r--r--tvsaver/saver2v3/save_file_test.go314
-rw-r--r--tvsaver/saver2v3/save_other_license.go32
-rw-r--r--tvsaver/saver2v3/save_other_license_test.go83
-rw-r--r--tvsaver/saver2v3/save_package.go129
-rw-r--r--tvsaver/saver2v3/save_package_test.go531
-rw-r--r--tvsaver/saver2v3/save_relationship.go24
-rw-r--r--tvsaver/saver2v3/save_relationship_test.go145
-rw-r--r--tvsaver/saver2v3/save_review.go26
-rw-r--r--tvsaver/saver2v3/save_review_test.go98
-rw-r--r--tvsaver/saver2v3/save_snippet.go55
-rw-r--r--tvsaver/saver2v3/save_snippet_test.go144
-rw-r--r--tvsaver/saver2v3/util.go16
-rw-r--r--tvsaver/saver2v3/util_test.go32
20 files changed, 2441 insertions, 0 deletions
diff --git a/tvsaver/saver2v3/save_annotation.go b/tvsaver/saver2v3/save_annotation.go
new file mode 100644
index 0000000..6c4f1aa
--- /dev/null
+++ b/tvsaver/saver2v3/save_annotation.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderAnnotation2_3(ann *v2_3.Annotation, w io.Writer) error {
+ if ann.Annotator.Annotator != "" && ann.Annotator.AnnotatorType != "" {
+ fmt.Fprintf(w, "Annotator: %s: %s\n", ann.Annotator.AnnotatorType, ann.Annotator.Annotator)
+ }
+ if ann.AnnotationDate != "" {
+ fmt.Fprintf(w, "AnnotationDate: %s\n", ann.AnnotationDate)
+ }
+ if ann.AnnotationType != "" {
+ fmt.Fprintf(w, "AnnotationType: %s\n", ann.AnnotationType)
+ }
+ annIDStr := common.RenderDocElementID(ann.AnnotationSPDXIdentifier)
+ if annIDStr != "SPDXRef-" {
+ fmt.Fprintf(w, "SPDXREF: %s\n", annIDStr)
+ }
+ if ann.AnnotationComment != "" {
+ fmt.Fprintf(w, "AnnotationComment: %s\n", textify(ann.AnnotationComment))
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_annotation_test.go b/tvsaver/saver2v3/save_annotation_test.go
new file mode 100644
index 0000000..7471260
--- /dev/null
+++ b/tvsaver/saver2v3/save_annotation_test.go
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Annotation section Saver tests =====
+func TestSaver2_3AnnotationSavesTextForPerson(t *testing.T) {
+ ann := &v2_3.Annotation{
+ Annotator: common.Annotator{AnnotatorType: "Person", Annotator: "John Doe"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Annotator: Person: John Doe
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderAnnotation2_3(ann, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3AnnotationSavesTextForOrganization(t *testing.T) {
+ ann := &v2_3.Annotation{
+ Annotator: common.Annotator{AnnotatorType: "Organization", Annotator: "John Doe, Inc."},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Annotator: Organization: John Doe, Inc.
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderAnnotation2_3(ann, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3AnnotationSavesTextForTool(t *testing.T) {
+ ann := &v2_3.Annotation{
+ Annotator: common.Annotator{AnnotatorType: "Tool", Annotator: "magictool-1.1"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Annotator: Tool: magictool-1.1
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderAnnotation2_3(ann, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+// note that the annotation has no optional or multiple fields
diff --git a/tvsaver/saver2v3/save_creation_info.go b/tvsaver/saver2v3/save_creation_info.go
new file mode 100644
index 0000000..2e8037d
--- /dev/null
+++ b/tvsaver/saver2v3/save_creation_info.go
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderCreationInfo2_3(ci *v2_3.CreationInfo, w io.Writer) error {
+ if ci.LicenseListVersion != "" {
+ fmt.Fprintf(w, "LicenseListVersion: %s\n", ci.LicenseListVersion)
+ }
+ for _, creator := range ci.Creators {
+ fmt.Fprintf(w, "Creator: %s: %s\n", creator.CreatorType, creator.Creator)
+ }
+ if ci.Created != "" {
+ fmt.Fprintf(w, "Created: %s\n", ci.Created)
+ }
+ if ci.CreatorComment != "" {
+ fmt.Fprintf(w, "CreatorComment: %s\n", textify(ci.CreatorComment))
+ }
+
+ // add blank newline b/c end of a main section
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_creation_info_test.go b/tvsaver/saver2v3/save_creation_info_test.go
new file mode 100644
index 0000000..a433dc5
--- /dev/null
+++ b/tvsaver/saver2v3/save_creation_info_test.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Creation Info section Saver tests =====
+func TestSaver2_3CISavesText(t *testing.T) {
+ ci := &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ {Creator: "Jane Doe (janedoe@example.com)", CreatorType: "Person"},
+ {Creator: "John Doe, Inc.", CreatorType: "Organization"},
+ {Creator: "Jane Doe LLC", CreatorType: "Organization"},
+ {Creator: "magictool1-1.0", CreatorType: "Tool"},
+ {Creator: "magictool2-1.0", CreatorType: "Tool"},
+ {Creator: "magictool3-1.0", CreatorType: "Tool"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ CreatorComment: "this is a creator comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`LicenseListVersion: 3.9
+Creator: Person: John Doe
+Creator: Person: Jane Doe (janedoe@example.com)
+Creator: Organization: John Doe, Inc.
+Creator: Organization: Jane Doe LLC
+Creator: Tool: magictool1-1.0
+Creator: Tool: magictool2-1.0
+Creator: Tool: magictool3-1.0
+Created: 2018-10-10T06:20:00Z
+CreatorComment: this is a creator comment
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderCreationInfo2_3(ci, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3CIOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ // --- need at least one creator; do first for Persons ---
+ ci1 := &v2_3.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want1 := bytes.NewBufferString(`Creator: Person: John Doe
+Created: 2018-10-10T06:20:00Z
+
+`)
+
+ // render as buffer of bytes
+ var got1 bytes.Buffer
+ err := renderCreationInfo2_3(ci1, &got1)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c1 := bytes.Compare(want1.Bytes(), got1.Bytes())
+ if c1 != 0 {
+ t.Errorf("Expected %v, got %v", want1.String(), got1.String())
+ }
+
+ // --- need at least one creator; now switch to organization ---
+ ci2 := &v2_3.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe, Inc.", CreatorType: "Organization"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want2 := bytes.NewBufferString(`Creator: Organization: John Doe, Inc.
+Created: 2018-10-10T06:20:00Z
+
+`)
+
+ // render as buffer of bytes
+ var got2 bytes.Buffer
+ err = renderCreationInfo2_3(ci2, &got2)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c2 := bytes.Compare(want2.Bytes(), got2.Bytes())
+ if c2 != 0 {
+ t.Errorf("Expected %v, got %v", want2.String(), got2.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_document.go b/tvsaver/saver2v3/save_document.go
new file mode 100644
index 0000000..e8c2535
--- /dev/null
+++ b/tvsaver/saver2v3/save_document.go
@@ -0,0 +1,104 @@
+// Package saver2v3 contains functions to render and write a tag-value
+// formatted version of an in-memory SPDX document and its sections
+// (version 2.2).
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// RenderDocument2_3 is the main entry point to take an SPDX in-memory
+// Document (version 2.2), and render it to the received io.Writer.
+// It is only exported in order to be available to the tvsaver package,
+// and typically does not need to be called by client code.
+func RenderDocument2_3(doc *v2_3.Document, w io.Writer) error {
+ if doc.CreationInfo == nil {
+ return fmt.Errorf("Document had nil CreationInfo section")
+ }
+
+ if doc.SPDXVersion != "" {
+ fmt.Fprintf(w, "SPDXVersion: %s\n", doc.SPDXVersion)
+ }
+ if doc.DataLicense != "" {
+ fmt.Fprintf(w, "DataLicense: %s\n", doc.DataLicense)
+ }
+ if doc.SPDXIdentifier != "" {
+ fmt.Fprintf(w, "SPDXID: %s\n", common.RenderElementID(doc.SPDXIdentifier))
+ }
+ if doc.DocumentName != "" {
+ fmt.Fprintf(w, "DocumentName: %s\n", doc.DocumentName)
+ }
+ if doc.DocumentNamespace != "" {
+ fmt.Fprintf(w, "DocumentNamespace: %s\n", doc.DocumentNamespace)
+ }
+ // print EDRs in order sorted by identifier
+ sort.Slice(doc.ExternalDocumentReferences, func(i, j int) bool {
+ return doc.ExternalDocumentReferences[i].DocumentRefID < doc.ExternalDocumentReferences[j].DocumentRefID
+ })
+ for _, edr := range doc.ExternalDocumentReferences {
+ fmt.Fprintf(w, "ExternalDocumentRef: DocumentRef-%s %s %s:%s\n",
+ edr.DocumentRefID, edr.URI, edr.Checksum.Algorithm, edr.Checksum.Value)
+ }
+ if doc.DocumentComment != "" {
+ fmt.Fprintf(w, "DocumentComment: %s\n", textify(doc.DocumentComment))
+ }
+
+ renderCreationInfo2_3(doc.CreationInfo, w)
+
+ if len(doc.Files) > 0 {
+ fmt.Fprintf(w, "##### Unpackaged files\n\n")
+ sort.Slice(doc.Files, func(i, j int) bool {
+ return doc.Files[i].FileSPDXIdentifier < doc.Files[j].FileSPDXIdentifier
+ })
+ for _, fi := range doc.Files {
+ renderFile2_3(fi, w)
+ }
+ }
+
+ // sort Packages by identifier
+ sort.Slice(doc.Packages, func(i, j int) bool {
+ return doc.Packages[i].PackageSPDXIdentifier < doc.Packages[j].PackageSPDXIdentifier
+ })
+ for _, pkg := range doc.Packages {
+ fmt.Fprintf(w, "##### Package: %s\n\n", pkg.PackageName)
+ renderPackage2_3(pkg, w)
+ }
+
+ if len(doc.OtherLicenses) > 0 {
+ fmt.Fprintf(w, "##### Other Licenses\n\n")
+ for _, ol := range doc.OtherLicenses {
+ renderOtherLicense2_3(ol, w)
+ }
+ }
+
+ if len(doc.Relationships) > 0 {
+ fmt.Fprintf(w, "##### Relationships\n\n")
+ for _, rln := range doc.Relationships {
+ renderRelationship2_3(rln, w)
+ }
+ fmt.Fprintf(w, "\n")
+ }
+
+ if len(doc.Annotations) > 0 {
+ fmt.Fprintf(w, "##### Annotations\n\n")
+ for _, ann := range doc.Annotations {
+ renderAnnotation2_3(ann, w)
+ fmt.Fprintf(w, "\n")
+ }
+ }
+
+ if len(doc.Reviews) > 0 {
+ fmt.Fprintf(w, "##### Reviews\n\n")
+ for _, rev := range doc.Reviews {
+ renderReview2_3(rev, w)
+ }
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_document_test.go b/tvsaver/saver2v3/save_document_test.go
new file mode 100644
index 0000000..10aa311
--- /dev/null
+++ b/tvsaver/saver2v3/save_document_test.go
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== entire Document Saver tests =====
+func TestSaver2_3DocumentSavesText(t *testing.T) {
+
+ // Creation Info section
+ ci := &v2_3.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // unpackaged files
+ f1 := &v2_3.File{
+ FileName: "/tmp/whatever1.txt",
+ FileSPDXIdentifier: common.ElementID("File1231"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983c", Algorithm: common.SHA1}},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ }
+
+ f2 := &v2_3.File{
+ FileName: "/tmp/whatever2.txt",
+ FileSPDXIdentifier: common.ElementID("File1232"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983d", Algorithm: common.SHA1}},
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{"MIT"},
+ FileCopyrightText: "Copyright (c) John Doe",
+ }
+
+ unFiles := []*v2_3.File{
+ f1,
+ f2,
+ }
+
+ // Package 1: packaged files with snippets
+ sn1 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: "Snippet19",
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "FileHasSnippets").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 17}, EndPointer: common.SnippetRangePointer{Offset: 209}}},
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ }
+
+ sn2 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: "Snippet20",
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "FileHasSnippets").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 268}, EndPointer: common.SnippetRangePointer{Offset: 309}}},
+ SnippetLicenseConcluded: "WTFPL",
+ SnippetCopyrightText: "NOASSERTION",
+ }
+
+ f3 := &v2_3.File{
+ FileName: "/tmp/file-with-snippets.txt",
+ FileSPDXIdentifier: common.ElementID("FileHasSnippets"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983e", Algorithm: common.SHA1}},
+ LicenseConcluded: "GPL-2.0-or-later AND WTFPL",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "WTFPL",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ Snippets: map[common.ElementID]*v2_3.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ },
+ }
+
+ f4 := &v2_3.File{
+ FileName: "/tmp/another-file.txt",
+ FileSPDXIdentifier: common.ElementID("FileAnother"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983f", Algorithm: common.SHA1}},
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{"BSD-3-Clause"},
+ FileCopyrightText: "Copyright (c) Jane Doe LLC",
+ }
+
+ pkgWith := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "0123456789abcdef0123456789abcdef01234567"},
+ PackageLicenseConcluded: "GPL-2.0-or-later AND BSD-3-Clause AND WTFPL",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "WTFPL",
+ "BSD-3-Clause",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ Files: []*v2_3.File{
+ f3,
+ f4,
+ },
+ }
+
+ // Other Licenses 1 and 2
+ ol1 := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ }
+
+ ol2 := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: `License 2 text - this is a license that does some stuff`,
+ LicenseName: "License 2",
+ }
+
+ // Relationships
+ rln1 := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln2 := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1231"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln3 := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1232"),
+ Relationship: "DESCRIBES",
+ }
+
+ // Annotations
+ ann1 := &v2_3.Annotation{
+ Annotator: common.Annotator{Annotator: "John Doe",
+ AnnotatorType: "Person"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ ann2 := &v2_3.Annotation{
+ Annotator: common.Annotator{Annotator: "John Doe, Inc.",
+ AnnotatorType: "Organization"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "p1"),
+ AnnotationComment: "This is an annotation about Package p1",
+ }
+
+ // Reviews
+ rev1 := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ }
+ rev2 := &v2_3.Review{
+ Reviewer: "Jane Doe LLC",
+ ReviewerType: "Organization",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ ReviewComment: "I have reviewed this SPDX document and it is awesome",
+ }
+
+ // now, build the document
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: "tools-golang-0.0.1.abcdef",
+ DocumentNamespace: "https://github.com/spdx/spdx-docs/tools-golang/tools-golang-0.0.1.abcdef.whatever",
+ CreationInfo: ci,
+ Packages: []*v2_3.Package{
+ pkgWith,
+ },
+ Files: unFiles,
+ OtherLicenses: []*v2_3.OtherLicense{
+ ol1,
+ ol2,
+ },
+ Relationships: []*v2_3.Relationship{
+ rln1,
+ rln2,
+ rln3,
+ },
+ Annotations: []*v2_3.Annotation{
+ ann1,
+ ann2,
+ },
+ Reviews: []*v2_3.Review{
+ rev1,
+ rev2,
+ },
+ }
+
+ want := bytes.NewBufferString(`SPDXVersion: SPDX-2.2
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: tools-golang-0.0.1.abcdef
+DocumentNamespace: https://github.com/spdx/spdx-docs/tools-golang/tools-golang-0.0.1.abcdef.whatever
+Creator: Person: John Doe
+Created: 2018-10-10T06:20:00Z
+
+##### Unpackaged files
+
+FileName: /tmp/whatever1.txt
+SPDXID: SPDXRef-File1231
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+FileName: /tmp/whatever2.txt
+SPDXID: SPDXRef-File1232
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983d
+LicenseConcluded: MIT
+LicenseInfoInFile: MIT
+FileCopyrightText: Copyright (c) John Doe
+
+##### Package: p1
+
+PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: true
+PackageVerificationCode: 0123456789abcdef0123456789abcdef01234567
+PackageLicenseConcluded: GPL-2.0-or-later AND BSD-3-Clause AND WTFPL
+PackageLicenseInfoFromFiles: Apache-2.0
+PackageLicenseInfoFromFiles: GPL-2.0-or-later
+PackageLicenseInfoFromFiles: WTFPL
+PackageLicenseInfoFromFiles: BSD-3-Clause
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+
+FileName: /tmp/another-file.txt
+SPDXID: SPDXRef-FileAnother
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983f
+LicenseConcluded: BSD-3-Clause
+LicenseInfoInFile: BSD-3-Clause
+FileCopyrightText: Copyright (c) Jane Doe LLC
+
+FileName: /tmp/file-with-snippets.txt
+SPDXID: SPDXRef-FileHasSnippets
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983e
+LicenseConcluded: GPL-2.0-or-later AND WTFPL
+LicenseInfoInFile: Apache-2.0
+LicenseInfoInFile: GPL-2.0-or-later
+LicenseInfoInFile: WTFPL
+FileCopyrightText: Copyright (c) Jane Doe
+
+SnippetSPDXID: SPDXRef-Snippet19
+SnippetFromFileSPDXID: SPDXRef-FileHasSnippets
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+
+SnippetSPDXID: SPDXRef-Snippet20
+SnippetFromFileSPDXID: SPDXRef-FileHasSnippets
+SnippetByteRange: 268:309
+SnippetLicenseConcluded: WTFPL
+SnippetCopyrightText: NOASSERTION
+
+##### Other Licenses
+
+LicenseID: LicenseRef-1
+ExtractedText: <text>License 1 text
+blah blah blah
+blah blah blah blah</text>
+LicenseName: License 1
+
+LicenseID: LicenseRef-2
+ExtractedText: License 2 text - this is a license that does some stuff
+LicenseName: License 2
+
+##### Relationships
+
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-p1
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File1231
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File1232
+
+##### Annotations
+
+Annotator: Person: John Doe
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+
+Annotator: Organization: John Doe, Inc.
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-p1
+AnnotationComment: This is an annotation about Package p1
+
+##### Reviews
+
+Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+
+Reviewer: Organization: Jane Doe LLC
+ReviewDate: 2018-10-14T10:28:00Z
+ReviewComment: I have reviewed this SPDX document and it is awesome
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := RenderDocument2_3(doc, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected {{{%v}}}, got {{{%v}}}", want.String(), got.String())
+ }
+
+}
+
+func TestSaver2_3DocumentReturnsErrorIfNilCreationInfo(t *testing.T) {
+ doc := &v2_3.Document{}
+
+ var got bytes.Buffer
+ err := RenderDocument2_3(doc, &got)
+ if err == nil {
+ t.Errorf("Expected error, got nil")
+ }
+}
diff --git a/tvsaver/saver2v3/save_file.go b/tvsaver/saver2v3/save_file.go
new file mode 100644
index 0000000..18a6d98
--- /dev/null
+++ b/tvsaver/saver2v3/save_file.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderFile2_3(f *v2_3.File, w io.Writer) error {
+ if f.FileName != "" {
+ fmt.Fprintf(w, "FileName: %s\n", f.FileName)
+ }
+ if f.FileSPDXIdentifier != "" {
+ fmt.Fprintf(w, "SPDXID: %s\n", common.RenderElementID(f.FileSPDXIdentifier))
+ }
+ for _, s := range f.FileTypes {
+ fmt.Fprintf(w, "FileType: %s\n", s)
+ }
+
+ for _, checksum := range f.Checksums {
+ fmt.Fprintf(w, "FileChecksum: %s: %s\n", checksum.Algorithm, checksum.Value)
+ }
+
+ if f.LicenseConcluded != "" {
+ fmt.Fprintf(w, "LicenseConcluded: %s\n", f.LicenseConcluded)
+ }
+ for _, s := range f.LicenseInfoInFiles {
+ fmt.Fprintf(w, "LicenseInfoInFile: %s\n", s)
+ }
+ if f.LicenseComments != "" {
+ fmt.Fprintf(w, "LicenseComments: %s\n", textify(f.LicenseComments))
+ }
+ if f.FileCopyrightText != "" {
+ fmt.Fprintf(w, "FileCopyrightText: %s\n", textify(f.FileCopyrightText))
+ }
+ for _, aop := range f.ArtifactOfProjects {
+ fmt.Fprintf(w, "ArtifactOfProjectName: %s\n", aop.Name)
+ if aop.HomePage != "" {
+ fmt.Fprintf(w, "ArtifactOfProjectHomePage: %s\n", aop.HomePage)
+ }
+ if aop.URI != "" {
+ fmt.Fprintf(w, "ArtifactOfProjectURI: %s\n", aop.URI)
+ }
+ }
+ if f.FileComment != "" {
+ fmt.Fprintf(w, "FileComment: %s\n", textify(f.FileComment))
+ }
+ if f.FileNotice != "" {
+ fmt.Fprintf(w, "FileNotice: %s\n", textify(f.FileNotice))
+ }
+ for _, s := range f.FileContributors {
+ fmt.Fprintf(w, "FileContributor: %s\n", s)
+ }
+ for _, s := range f.FileAttributionTexts {
+ fmt.Fprintf(w, "FileAttributionText: %s\n", textify(s))
+ }
+ for _, s := range f.FileDependencies {
+ fmt.Fprintf(w, "FileDependency: %s\n", s)
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ // also render any snippets for this file
+ // get slice of Snippet identifiers so we can sort them
+ snippetKeys := []string{}
+ for k := range f.Snippets {
+ snippetKeys = append(snippetKeys, string(k))
+ }
+ sort.Strings(snippetKeys)
+ for _, sID := range snippetKeys {
+ s := f.Snippets[common.ElementID(sID)]
+ renderSnippet2_3(s, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_file_test.go b/tvsaver/saver2v3/save_file_test.go
new file mode 100644
index 0000000..05fc2cf
--- /dev/null
+++ b/tvsaver/saver2v3/save_file_test.go
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== File section Saver tests =====
+func TestSaver2_3FileSavesText(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ FileTypes: []string{
+ "TEXT",
+ "DOCUMENTATION",
+ },
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ {Algorithm: common.SHA256, Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"},
+ {Algorithm: common.MD5, Value: "624c1abb3664f4b35547e7c73864ad24"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ "Apache-1.1",
+ },
+ LicenseComments: "this is a license comment(s)",
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ ArtifactOfProjects: []*v2_3.ArtifactOfProject{
+ {
+ Name: "project1",
+ HomePage: "http://example.com/1/",
+ URI: "http://example.com/1/uri.whatever",
+ },
+ {
+ Name: "project2",
+ },
+ {
+ Name: "project3",
+ HomePage: "http://example.com/3/",
+ },
+ {
+ Name: "project4",
+ URI: "http://example.com/4/uri.whatever",
+ },
+ },
+ FileComment: "this is a file comment",
+ FileNotice: "This file may be used under either Apache-2.0 or Apache-1.1.",
+ FileContributors: []string{
+ "John Doe jdoe@example.com",
+ "EvilCorp",
+ },
+ FileAttributionTexts: []string{
+ "attributions",
+ `multi-line
+attribution`,
+ },
+ FileDependencies: []string{
+ "f-1.txt",
+ "g.txt",
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileType: TEXT
+FileType: DOCUMENTATION
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+FileChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+FileChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+LicenseInfoInFile: Apache-1.1
+LicenseComments: this is a license comment(s)
+FileCopyrightText: Copyright (c) Jane Doe
+ArtifactOfProjectName: project1
+ArtifactOfProjectHomePage: http://example.com/1/
+ArtifactOfProjectURI: http://example.com/1/uri.whatever
+ArtifactOfProjectName: project2
+ArtifactOfProjectName: project3
+ArtifactOfProjectHomePage: http://example.com/3/
+ArtifactOfProjectName: project4
+ArtifactOfProjectURI: http://example.com/4/uri.whatever
+FileComment: this is a file comment
+FileNotice: This file may be used under either Apache-2.0 or Apache-1.1.
+FileContributor: John Doe jdoe@example.com
+FileContributor: EvilCorp
+FileAttributionText: attributions
+FileAttributionText: <text>multi-line
+attribution</text>
+FileDependency: f-1.txt
+FileDependency: g.txt
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileSavesSnippetsAlso(t *testing.T) {
+ sn1 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet19"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File123").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 17}, EndPointer: common.SnippetRangePointer{Offset: 209}}},
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ }
+
+ sn2 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet20"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File123").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 268}, EndPointer: common.SnippetRangePointer{Offset: 309}}},
+ SnippetLicenseConcluded: "WTFPL",
+ SnippetCopyrightText: "NOASSERTION",
+ }
+
+ sns := map[common.ElementID]*v2_3.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ }
+
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ Snippets: sns,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+SnippetSPDXID: SPDXRef-Snippet19
+SnippetFromFileSPDXID: SPDXRef-File123
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+
+SnippetSPDXID: SPDXRef-Snippet20
+SnippetFromFileSPDXID: SPDXRef-File123
+SnippetByteRange: 268:309
+SnippetLicenseConcluded: WTFPL
+SnippetCopyrightText: NOASSERTION
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileWrapsCopyrightMultiLine(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: `Copyright (c) Jane Doe
+Copyright (c) John Doe`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: <text>Copyright (c) Jane Doe
+Copyright (c) John Doe</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileWrapsCommentsAndNoticesMultiLine(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseComments: `this is a
+multi-line license comment`,
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ FileComment: `this is a
+multi-line file comment`,
+ FileNotice: `This file may be used
+under either Apache-2.0 or Apache-1.1.`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+LicenseComments: <text>this is a
+multi-line license comment</text>
+FileCopyrightText: Copyright (c) Jane Doe
+FileComment: <text>this is a
+multi-line file comment</text>
+FileNotice: <text>This file may be used
+under either Apache-2.0 or Apache-1.1.</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_other_license.go b/tvsaver/saver2v3/save_other_license.go
new file mode 100644
index 0000000..9351108
--- /dev/null
+++ b/tvsaver/saver2v3/save_other_license.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderOtherLicense2_3(ol *v2_3.OtherLicense, w io.Writer) error {
+ if ol.LicenseIdentifier != "" {
+ fmt.Fprintf(w, "LicenseID: %s\n", ol.LicenseIdentifier)
+ }
+ if ol.ExtractedText != "" {
+ fmt.Fprintf(w, "ExtractedText: %s\n", textify(ol.ExtractedText))
+ }
+ if ol.LicenseName != "" {
+ fmt.Fprintf(w, "LicenseName: %s\n", ol.LicenseName)
+ }
+ for _, s := range ol.LicenseCrossReferences {
+ fmt.Fprintf(w, "LicenseCrossReference: %s\n", s)
+ }
+ if ol.LicenseComment != "" {
+ fmt.Fprintf(w, "LicenseComment: %s\n", textify(ol.LicenseComment))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_other_license_test.go b/tvsaver/saver2v3/save_other_license_test.go
new file mode 100644
index 0000000..00134dd
--- /dev/null
+++ b/tvsaver/saver2v3/save_other_license_test.go
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Other License section Saver tests =====
+func TestSaver2_3OtherLicenseSavesText(t *testing.T) {
+ ol := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ LicenseCrossReferences: []string{
+ "http://example.com/License1/",
+ "http://example.com/License1AnotherURL/",
+ },
+ LicenseComment: "this is a license comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`LicenseID: LicenseRef-1
+ExtractedText: <text>License 1 text
+blah blah blah
+blah blah blah blah</text>
+LicenseName: License 1
+LicenseCrossReference: http://example.com/License1/
+LicenseCrossReference: http://example.com/License1AnotherURL/
+LicenseComment: this is a license comment
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderOtherLicense2_3(ol, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3OtherLicenseOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ ol := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`LicenseID: LicenseRef-1
+ExtractedText: <text>License 1 text
+blah blah blah
+blah blah blah blah</text>
+LicenseName: License 1
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderOtherLicense2_3(ol, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_package.go b/tvsaver/saver2v3/save_package.go
new file mode 100644
index 0000000..d323af9
--- /dev/null
+++ b/tvsaver/saver2v3/save_package.go
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderPackage2_3(pkg *v2_3.Package, w io.Writer) error {
+ if pkg.PackageName != "" {
+ fmt.Fprintf(w, "PackageName: %s\n", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != "" {
+ fmt.Fprintf(w, "SPDXID: %s\n", common.RenderElementID(pkg.PackageSPDXIdentifier))
+ }
+ if pkg.PackageVersion != "" {
+ fmt.Fprintf(w, "PackageVersion: %s\n", pkg.PackageVersion)
+ }
+ if pkg.PackageFileName != "" {
+ fmt.Fprintf(w, "PackageFileName: %s\n", pkg.PackageFileName)
+ }
+ if pkg.PackageSupplier != nil && pkg.PackageSupplier.Supplier != "" {
+ if pkg.PackageSupplier.SupplierType == "" {
+ fmt.Fprintf(w, "PackageSupplier: %s\n", pkg.PackageSupplier.Supplier)
+ } else {
+ fmt.Fprintf(w, "PackageSupplier: %s: %s\n", pkg.PackageSupplier.SupplierType, pkg.PackageSupplier.Supplier)
+ }
+ }
+ if pkg.PackageOriginator != nil && pkg.PackageOriginator.Originator != "" {
+ if pkg.PackageOriginator.OriginatorType == "" {
+ fmt.Fprintf(w, "PackageOriginator: %s\n", pkg.PackageOriginator.Originator)
+ } else {
+ fmt.Fprintf(w, "PackageOriginator: %s: %s\n", pkg.PackageOriginator.OriginatorType, pkg.PackageOriginator.Originator)
+ }
+ }
+ if pkg.PackageDownloadLocation != "" {
+ fmt.Fprintf(w, "PackageDownloadLocation: %s\n", pkg.PackageDownloadLocation)
+ }
+ if pkg.PrimaryPackagePurpose != "" {
+ fmt.Fprintf(w, "PrimaryPackagePurpose: %s\n", pkg.PrimaryPackagePurpose)
+ }
+ if pkg.ReleaseDate != "" {
+ fmt.Fprintf(w, "ReleaseDate: %s\n", pkg.ReleaseDate)
+ }
+ if pkg.BuiltDate != "" {
+ fmt.Fprintf(w, "BuiltDate: %s\n", pkg.BuiltDate)
+ }
+ if pkg.ValidUntilDate != "" {
+ fmt.Fprintf(w, "ValidUntilDate: %s\n", pkg.ValidUntilDate)
+ }
+ if pkg.FilesAnalyzed == true {
+ if pkg.IsFilesAnalyzedTagPresent == true {
+ fmt.Fprintf(w, "FilesAnalyzed: true\n")
+ }
+ } else {
+ fmt.Fprintf(w, "FilesAnalyzed: false\n")
+ }
+ if pkg.PackageVerificationCode != nil && pkg.PackageVerificationCode.Value != "" && pkg.FilesAnalyzed == true {
+ if len(pkg.PackageVerificationCode.ExcludedFiles) == 0 {
+ fmt.Fprintf(w, "PackageVerificationCode: %s\n", pkg.PackageVerificationCode.Value)
+ } else {
+ fmt.Fprintf(w, "PackageVerificationCode: %s (excludes: %s)\n", pkg.PackageVerificationCode.Value, strings.Join(pkg.PackageVerificationCode.ExcludedFiles, ", "))
+ }
+ }
+
+ for _, checksum := range pkg.PackageChecksums {
+ fmt.Fprintf(w, "PackageChecksum: %s: %s\n", checksum.Algorithm, checksum.Value)
+ }
+
+ if pkg.PackageHomePage != "" {
+ fmt.Fprintf(w, "PackageHomePage: %s\n", pkg.PackageHomePage)
+ }
+ if pkg.PackageSourceInfo != "" {
+ fmt.Fprintf(w, "PackageSourceInfo: %s\n", textify(pkg.PackageSourceInfo))
+ }
+ if pkg.PackageLicenseConcluded != "" {
+ fmt.Fprintf(w, "PackageLicenseConcluded: %s\n", pkg.PackageLicenseConcluded)
+ }
+ if pkg.FilesAnalyzed == true {
+ for _, s := range pkg.PackageLicenseInfoFromFiles {
+ fmt.Fprintf(w, "PackageLicenseInfoFromFiles: %s\n", s)
+ }
+ }
+ if pkg.PackageLicenseDeclared != "" {
+ fmt.Fprintf(w, "PackageLicenseDeclared: %s\n", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageLicenseComments != "" {
+ fmt.Fprintf(w, "PackageLicenseComments: %s\n", textify(pkg.PackageLicenseComments))
+ }
+ if pkg.PackageCopyrightText != "" {
+ fmt.Fprintf(w, "PackageCopyrightText: %s\n", textify(pkg.PackageCopyrightText))
+ }
+ if pkg.PackageSummary != "" {
+ fmt.Fprintf(w, "PackageSummary: %s\n", textify(pkg.PackageSummary))
+ }
+ if pkg.PackageDescription != "" {
+ fmt.Fprintf(w, "PackageDescription: %s\n", textify(pkg.PackageDescription))
+ }
+ if pkg.PackageComment != "" {
+ fmt.Fprintf(w, "PackageComment: %s\n", textify(pkg.PackageComment))
+ }
+ for _, s := range pkg.PackageExternalReferences {
+ fmt.Fprintf(w, "ExternalRef: %s %s %s\n", s.Category, s.RefType, s.Locator)
+ if s.ExternalRefComment != "" {
+ fmt.Fprintf(w, "ExternalRefComment: %s\n", textify(s.ExternalRefComment))
+ }
+ }
+ for _, s := range pkg.PackageAttributionTexts {
+ fmt.Fprintf(w, "PackageAttributionText: %s\n", textify(s))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ // also render any files for this package
+ sort.Slice(pkg.Files, func(i, j int) bool {
+ return pkg.Files[i].FileSPDXIdentifier < pkg.Files[j].FileSPDXIdentifier
+ })
+ for _, fi := range pkg.Files {
+ renderFile2_3(fi, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_package_test.go b/tvsaver/saver2v3/save_package_test.go
new file mode 100644
index 0000000..435b5b5
--- /dev/null
+++ b/tvsaver/saver2v3/save_package_test.go
@@ -0,0 +1,531 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Package section Saver tests =====
+func TestSaver2_3PackageSavesTextCombo1(t *testing.T) {
+ // include package external refs
+ // test Supplier:Organization, Originator:Person
+ // FilesAnalyzed true, IsFilesAnalyzedTagPresent true
+ // PackageVerificationCodeExcludedFile has string
+
+ // NOTE, this is an entirely made up CPE and the format is likely invalid
+ per1 := &v2_3.PackageExternalReference{
+ Category: "SECURITY",
+ RefType: "cpe22Type",
+ Locator: "cpe:/a:john_doe_inc:p1:0.1.0",
+ ExternalRefComment: "this is an external ref comment #1",
+ }
+
+ // NOTE, this is an entirely made up NPM
+ per2 := &v2_3.PackageExternalReference{
+ Category: "PACKAGE-MANAGER",
+ RefType: "npm",
+ Locator: "p1@0.1.0",
+ ExternalRefComment: `this is a
+multi-line external ref comment`,
+ }
+
+ // NOTE, this is an entirely made up SWH persistent ID
+ per3 := &v2_3.PackageExternalReference{
+ Category: "PERSISTENT-ID",
+ RefType: "swh",
+ Locator: "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2",
+ // no ExternalRefComment for this one
+ }
+
+ per4 := &v2_3.PackageExternalReference{
+ Category: "OTHER",
+ RefType: "anything",
+ Locator: "anything-without-spaces-can-go-here",
+ // no ExternalRefComment for this one
+ }
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageVersion: "0.1.0",
+ PackageFileName: "p1-0.1.0-master.tar.gz",
+ PackageSupplier: &common.Supplier{SupplierType: "Organization", Supplier: "John Doe, Inc."},
+ PackageOriginator: &common.Originator{Originator: "John Doe", OriginatorType: "Person"},
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "0123456789abcdef0123456789abcdef01234567",
+ ExcludedFiles: []string{"p1-0.1.0.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ {
+ Algorithm: common.MD5,
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ PackageHomePage: "http://example.com/p1",
+ PackageSourceInfo: "this is a source comment",
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageLicenseComments: "this is a license comment(s)",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ PackageSummary: "this is a summary comment",
+ PackageDescription: "this is a description comment",
+ PackageComment: "this is a comment comment",
+ PackageAttributionTexts: []string{"Include this notice in all advertising materials"},
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ per1,
+ per2,
+ per3,
+ per4,
+ },
+ PrimaryPackagePurpose: "LIBRARY",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageVersion: 0.1.0
+PackageFileName: p1-0.1.0-master.tar.gz
+PackageSupplier: Organization: John Doe, Inc.
+PackageOriginator: Person: John Doe
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+PrimaryPackagePurpose: LIBRARY
+ReleaseDate: 2021-10-15T02:38:00Z
+BuiltDate: 2021-09-15T02:38:00Z
+ValidUntilDate: 2022-10-15T02:38:00Z
+FilesAnalyzed: true
+PackageVerificationCode: 0123456789abcdef0123456789abcdef01234567 (excludes: p1-0.1.0.spdx)
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageHomePage: http://example.com/p1
+PackageSourceInfo: this is a source comment
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseInfoFromFiles: Apache-1.1
+PackageLicenseInfoFromFiles: Apache-2.0
+PackageLicenseInfoFromFiles: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageLicenseComments: this is a license comment(s)
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+PackageSummary: this is a summary comment
+PackageDescription: this is a description comment
+PackageComment: this is a comment comment
+ExternalRef: SECURITY cpe22Type cpe:/a:john_doe_inc:p1:0.1.0
+ExternalRefComment: this is an external ref comment #1
+ExternalRef: PACKAGE-MANAGER npm p1@0.1.0
+ExternalRefComment: <text>this is a
+multi-line external ref comment</text>
+ExternalRef: PERSISTENT-ID swh swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2
+ExternalRef: OTHER anything anything-without-spaces-can-go-here
+PackageAttributionText: Include this notice in all advertising materials
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSavesTextCombo2(t *testing.T) {
+ // no package external refs
+ // test Supplier:NOASSERTION, Originator:Organization
+ // FilesAnalyzed true, IsFilesAnalyzedTagPresent false
+ // PackageVerificationCodeExcludedFile is empty
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageVersion: "0.1.0",
+ PackageFileName: "p1-0.1.0-master.tar.gz",
+ PackageSupplier: &common.Supplier{Supplier: "NOASSERTION"},
+ PackageOriginator: &common.Originator{OriginatorType: "Organization", Originator: "John Doe, Inc."},
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "0123456789abcdef0123456789abcdef01234567"},
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ {
+ Algorithm: common.MD5,
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ PackageHomePage: "http://example.com/p1",
+ PackageSourceInfo: "this is a source comment",
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageLicenseComments: "this is a license comment(s)",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ PackageSummary: "this is a summary comment",
+ PackageDescription: "this is a description comment",
+ PackageComment: "this is a comment comment",
+ PackageAttributionTexts: []string{"Include this notice in all advertising materials"},
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageVersion: 0.1.0
+PackageFileName: p1-0.1.0-master.tar.gz
+PackageSupplier: NOASSERTION
+PackageOriginator: Organization: John Doe, Inc.
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+PackageVerificationCode: 0123456789abcdef0123456789abcdef01234567
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageHomePage: http://example.com/p1
+PackageSourceInfo: this is a source comment
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseInfoFromFiles: Apache-1.1
+PackageLicenseInfoFromFiles: Apache-2.0
+PackageLicenseInfoFromFiles: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageLicenseComments: this is a license comment(s)
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+PackageSummary: this is a summary comment
+PackageDescription: this is a description comment
+PackageComment: this is a comment comment
+PackageAttributionText: Include this notice in all advertising materials
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSavesTextCombo3(t *testing.T) {
+ // no package external refs
+ // test Supplier:Person, Originator:NOASSERTION
+ // FilesAnalyzed false, IsFilesAnalyzedTagPresent true
+ // PackageVerificationCodeExcludedFile is empty
+ // three PackageAttributionTexts, one with multi-line text
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageVersion: "0.1.0",
+ PackageFileName: "p1-0.1.0-master.tar.gz",
+ PackageSupplier: &common.Supplier{Supplier: "John Doe", SupplierType: "Person"},
+ PackageOriginator: &common.Originator{Originator: "NOASSERTION"},
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ // NOTE that verification code MUST be omitted from output
+ // since FilesAnalyzed is false
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "0123456789abcdef0123456789abcdef01234567"},
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ {
+ Algorithm: common.MD5,
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ PackageHomePage: "http://example.com/p1",
+ PackageSourceInfo: "this is a source comment",
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ // NOTE that license info from files MUST be omitted from output
+ // since FilesAnalyzed is false
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageLicenseComments: "this is a license comment(s)",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ PackageSummary: "this is a summary comment",
+ PackageDescription: "this is a description comment",
+ PackageComment: "this is a comment comment",
+ PackageAttributionTexts: []string{
+ "Include this notice in all advertising materials",
+ "and also this notice",
+ `and this multi-line notice
+which goes across two lines`,
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageVersion: 0.1.0
+PackageFileName: p1-0.1.0-master.tar.gz
+PackageSupplier: Person: John Doe
+PackageOriginator: NOASSERTION
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageHomePage: http://example.com/p1
+PackageSourceInfo: this is a source comment
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageLicenseComments: this is a license comment(s)
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+PackageSummary: this is a summary comment
+PackageDescription: this is a description comment
+PackageComment: this is a comment comment
+PackageAttributionText: Include this notice in all advertising materials
+PackageAttributionText: and also this notice
+PackageAttributionText: <text>and this multi-line notice
+which goes across two lines</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSaveOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ // NOTE that verification code MUST be omitted from output,
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ // NOTE that license info from files MUST be omitted from output
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSavesFilesIfPresent(t *testing.T) {
+ f1 := &v2_3.File{
+ FileName: "/tmp/whatever1.txt",
+ FileSPDXIdentifier: common.ElementID("File1231"),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ }
+
+ f2 := &v2_3.File{
+ FileName: "/tmp/whatever2.txt",
+ FileSPDXIdentifier: common.ElementID("File1232"),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983d",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{"MIT"},
+ FileCopyrightText: "Copyright (c) John Doe",
+ }
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ // NOTE that verification code MUST be omitted from output,
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ // NOTE that license info from files MUST be omitted from output
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ Files: []*v2_3.File{
+ f1,
+ f2,
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+
+FileName: /tmp/whatever1.txt
+SPDXID: SPDXRef-File1231
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+FileName: /tmp/whatever2.txt
+SPDXID: SPDXRef-File1232
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983d
+LicenseConcluded: MIT
+LicenseInfoInFile: MIT
+FileCopyrightText: Copyright (c) John Doe
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageWrapsMultiLine(t *testing.T) {
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: `Copyright (c) John Doe, Inc.
+Copyright Jane Doe`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: <text>Copyright (c) John Doe, Inc.
+Copyright Jane Doe</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_relationship.go b/tvsaver/saver2v3/save_relationship.go
new file mode 100644
index 0000000..c83310f
--- /dev/null
+++ b/tvsaver/saver2v3/save_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderRelationship2_3(rln *v2_3.Relationship, w io.Writer) error {
+ rlnAStr := common.RenderDocElementID(rln.RefA)
+ rlnBStr := common.RenderDocElementID(rln.RefB)
+ if rlnAStr != "SPDXRef-" && rlnBStr != "SPDXRef-" && rln.Relationship != "" {
+ fmt.Fprintf(w, "Relationship: %s %s %s\n", rlnAStr, rln.Relationship, rlnBStr)
+ }
+ if rln.RelationshipComment != "" {
+ fmt.Fprintf(w, "RelationshipComment: %s\n", textify(rln.RelationshipComment))
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_relationship_test.go b/tvsaver/saver2v3/save_relationship_test.go
new file mode 100644
index 0000000..26ce0c3
--- /dev/null
+++ b/tvsaver/saver2v3/save_relationship_test.go
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Relationship section Saver tests =====
+func TestSaver2_3RelationshipSavesText(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "2"),
+ Relationship: "DESCRIBES",
+ RelationshipComment: "this is a comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-2
+RelationshipComment: this is a comment
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "2"),
+ Relationship: "DESCRIBES",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString("Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-2\n")
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipCanHaveNONEOnRight(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "PackageA"),
+ RefB: common.MakeDocElementSpecial("NONE"),
+ Relationship: "DEPENDS_ON",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString("Relationship: SPDXRef-PackageA DEPENDS_ON NONE\n")
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipCanHaveNOASSERTIONOnRight(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "PackageA"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "DEPENDS_ON",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString("Relationship: SPDXRef-PackageA DEPENDS_ON NOASSERTION\n")
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipWrapsCommentMultiLine(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "2"),
+ Relationship: "DESCRIBES",
+ RelationshipComment: `this is a
+multi-line comment`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-2
+RelationshipComment: <text>this is a
+multi-line comment</text>
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_review.go b/tvsaver/saver2v3/save_review.go
new file mode 100644
index 0000000..72bac11
--- /dev/null
+++ b/tvsaver/saver2v3/save_review.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderReview2_3(rev *v2_3.Review, w io.Writer) error {
+ if rev.Reviewer != "" && rev.ReviewerType != "" {
+ fmt.Fprintf(w, "Reviewer: %s: %s\n", rev.ReviewerType, rev.Reviewer)
+ }
+ if rev.ReviewDate != "" {
+ fmt.Fprintf(w, "ReviewDate: %s\n", rev.ReviewDate)
+ }
+ if rev.ReviewComment != "" {
+ fmt.Fprintf(w, "ReviewComment: %s\n", textify(rev.ReviewComment))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_review_test.go b/tvsaver/saver2v3/save_review_test.go
new file mode 100644
index 0000000..5eec3bc
--- /dev/null
+++ b/tvsaver/saver2v3/save_review_test.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Review section Saver tests =====
+func TestSaver2_3ReviewSavesText(t *testing.T) {
+ rev := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ ReviewComment: "this is a review comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+ReviewComment: this is a review comment
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderReview2_3(rev, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3ReviewOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rev := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderReview2_3(rev, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3ReviewWrapsMultiLine(t *testing.T) {
+ rev := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ ReviewComment: `this is a
+multi-line review comment`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+ReviewComment: <text>this is a
+multi-line review comment</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderReview2_3(rev, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_snippet.go b/tvsaver/saver2v3/save_snippet.go
new file mode 100644
index 0000000..2ba4a3b
--- /dev/null
+++ b/tvsaver/saver2v3/save_snippet.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderSnippet2_3(sn *v2_3.Snippet, w io.Writer) error {
+ if sn.SnippetSPDXIdentifier != "" {
+ fmt.Fprintf(w, "SnippetSPDXID: %s\n", common.RenderElementID(sn.SnippetSPDXIdentifier))
+ }
+ snFromFileIDStr := common.RenderElementID(sn.SnippetFromFileSPDXIdentifier)
+ if snFromFileIDStr != "" {
+ fmt.Fprintf(w, "SnippetFromFileSPDXID: %s\n", snFromFileIDStr)
+ }
+
+ for _, snippetRange := range sn.Ranges {
+ if snippetRange.StartPointer.Offset != 0 && snippetRange.EndPointer.Offset != 0 {
+ fmt.Fprintf(w, "SnippetByteRange: %d:%d\n", snippetRange.StartPointer.Offset, snippetRange.EndPointer.Offset)
+ }
+ if snippetRange.StartPointer.LineNumber != 0 && snippetRange.EndPointer.LineNumber != 0 {
+ fmt.Fprintf(w, "SnippetLineRange: %d:%d\n", snippetRange.StartPointer.LineNumber, snippetRange.EndPointer.LineNumber)
+ }
+ }
+ if sn.SnippetLicenseConcluded != "" {
+ fmt.Fprintf(w, "SnippetLicenseConcluded: %s\n", sn.SnippetLicenseConcluded)
+ }
+ for _, s := range sn.LicenseInfoInSnippet {
+ fmt.Fprintf(w, "LicenseInfoInSnippet: %s\n", s)
+ }
+ if sn.SnippetLicenseComments != "" {
+ fmt.Fprintf(w, "SnippetLicenseComments: %s\n", textify(sn.SnippetLicenseComments))
+ }
+ if sn.SnippetCopyrightText != "" {
+ fmt.Fprintf(w, "SnippetCopyrightText: %s\n", textify(sn.SnippetCopyrightText))
+ }
+ if sn.SnippetComment != "" {
+ fmt.Fprintf(w, "SnippetComment: %s\n", textify(sn.SnippetComment))
+ }
+ if sn.SnippetName != "" {
+ fmt.Fprintf(w, "SnippetName: %s\n", sn.SnippetName)
+ }
+ for _, s := range sn.SnippetAttributionTexts {
+ fmt.Fprintf(w, "SnippetAttributionText: %s\n", textify(s))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_snippet_test.go b/tvsaver/saver2v3/save_snippet_test.go
new file mode 100644
index 0000000..ef10194
--- /dev/null
+++ b/tvsaver/saver2v3/save_snippet_test.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Snippet section Saver tests =====
+func TestSaver2_3SnippetSavesText(t *testing.T) {
+ sn := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet17"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File292").ElementRefID,
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{LineNumber: 3},
+ EndPointer: common.SnippetRangePointer{LineNumber: 8},
+ },
+ {
+ StartPointer: common.SnippetRangePointer{Offset: 17},
+ EndPointer: common.SnippetRangePointer{Offset: 209},
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInSnippet: []string{
+ "GPL-2.0-or-later",
+ "MIT",
+ },
+ SnippetLicenseComments: "this is a comment(s) about the snippet license",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ SnippetComment: "this is a snippet comment",
+ SnippetName: "from John's program",
+ SnippetAttributionTexts: []string{"some attributions"},
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`SnippetSPDXID: SPDXRef-Snippet17
+SnippetFromFileSPDXID: SPDXRef-File292
+SnippetLineRange: 3:8
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+LicenseInfoInSnippet: GPL-2.0-or-later
+LicenseInfoInSnippet: MIT
+SnippetLicenseComments: this is a comment(s) about the snippet license
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+SnippetComment: this is a snippet comment
+SnippetName: from John's program
+SnippetAttributionText: some attributions
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_3(sn, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3SnippetOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ sn := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet17"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File292").ElementRefID,
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{Offset: 17},
+ EndPointer: common.SnippetRangePointer{Offset: 209},
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`SnippetSPDXID: SPDXRef-Snippet17
+SnippetFromFileSPDXID: SPDXRef-File292
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_3(sn, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3SnippetWrapsCopyrightMultiline(t *testing.T) {
+ sn := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet17"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File292").ElementRefID,
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{Offset: 17},
+ EndPointer: common.SnippetRangePointer{Offset: 209},
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: `Copyright (c) John Doe 20x6
+Copyright (c) John Doe 20x6`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`SnippetSPDXID: SPDXRef-Snippet17
+SnippetFromFileSPDXID: SPDXRef-File292
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: <text>Copyright (c) John Doe 20x6
+Copyright (c) John Doe 20x6</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_3(sn, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/util.go b/tvsaver/saver2v3/util.go
new file mode 100644
index 0000000..4dec724
--- /dev/null
+++ b/tvsaver/saver2v3/util.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "strings"
+)
+
+func textify(s string) string {
+ if strings.Contains(s, "\n") {
+ return fmt.Sprintf("<text>%s</text>", s)
+ }
+
+ return s
+}
diff --git a/tvsaver/saver2v3/util_test.go b/tvsaver/saver2v3/util_test.go
new file mode 100644
index 0000000..b4b7553
--- /dev/null
+++ b/tvsaver/saver2v3/util_test.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "testing"
+)
+
+// ===== Utility function tests =====
+func TestTextifyWrapsStringWithNewline(t *testing.T) {
+ s := `this text has
+a newline in it`
+ want := `<text>this text has
+a newline in it</text>`
+
+ got := textify(s)
+
+ if want != got {
+ t.Errorf("Expected %s, got %s", want, got)
+ }
+}
+
+func TestTextifyDoesNotWrapsStringWithNoNewline(t *testing.T) {
+ s := `this text has no newline in it`
+ want := s
+
+ got := textify(s)
+
+ if want != got {
+ t.Errorf("Expected %s, got %s", want, got)
+ }
+}