aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrandon Lum <lumjjb@gmail.com>2022-10-31 16:21:07 -0400
committerGitHub <noreply@github.com>2022-10-31 16:21:07 -0400
commite20ab888a04478990a42fc816a3bd188364fb9ce (patch)
tree3334341a7baf56335d158f35c11fc6d57e5c2e3e
parentfb7fe8874d01f3c9e2d7f450cf1407e7513b02ee (diff)
parentbc625283024d89fd4cea91df08bcd8c0c0a79405 (diff)
downloadspdx-tools-e20ab888a04478990a42fc816a3bd188364fb9ce.tar.gz
Merge pull request #164 from kzantow-anchore/feat/spdx-2.3-support
Add support for SPDX 2.3
-rw-r--r--builder/build.go71
-rw-r--r--builder/builder2v3/build_creation_info.go47
-rw-r--r--builder/builder2v3/build_creation_info_test.go111
-rw-r--r--builder/builder2v3/build_file.go56
-rw-r--r--builder/builder2v3/build_file_test.go74
-rw-r--r--builder/builder2v3/build_package.go79
-rw-r--r--builder/builder2v3/build_package_test.go156
-rw-r--r--builder/builder2v3/build_relationship.go24
-rw-r--r--builder/builder2v3/build_relationship_test.go33
-rw-r--r--examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json406
-rw-r--r--examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf2
-rw-r--r--examples/sample-docs/tv/SPDXTagExample-v2.3.spdx328
-rw-r--r--examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml412
-rw-r--r--idsearcher/idsearcher.go125
-rw-r--r--idsearcher/idsearcher_test.go289
-rw-r--r--json/json_v2_2_test.go (renamed from json/json_test.go)6
-rw-r--r--json/json_v2_3_test.go490
-rw-r--r--json/parser.go19
-rw-r--r--json/writer.go16
-rw-r--r--licensediff/licensediff.go29
-rw-r--r--licensediff/licensediff_test.go555
-rw-r--r--rdfloader/parser2v3/constants.go268
-rw-r--r--rdfloader/parser2v3/license_utils.go119
-rw-r--r--rdfloader/parser2v3/license_utils_test.go345
-rw-r--r--rdfloader/parser2v3/parse_annotation.go81
-rw-r--r--rdfloader/parser2v3/parse_annotation_test.go183
-rw-r--r--rdfloader/parser2v3/parse_creation_info.go58
-rw-r--r--rdfloader/parser2v3/parse_creation_info_test.go107
-rw-r--r--rdfloader/parser2v3/parse_file.go207
-rw-r--r--rdfloader/parser2v3/parse_file_test.go765
-rw-r--r--rdfloader/parser2v3/parse_license.go291
-rw-r--r--rdfloader/parser2v3/parse_license_test.go853
-rw-r--r--rdfloader/parser2v3/parse_other_license_info.go38
-rw-r--r--rdfloader/parser2v3/parse_other_license_info_test.go74
-rw-r--r--rdfloader/parser2v3/parse_package.go362
-rw-r--r--rdfloader/parser2v3/parse_package_test.go788
-rw-r--r--rdfloader/parser2v3/parse_relationship.go156
-rw-r--r--rdfloader/parser2v3/parse_relationship_test.go397
-rw-r--r--rdfloader/parser2v3/parse_review.go38
-rw-r--r--rdfloader/parser2v3/parse_review_test.go73
-rw-r--r--rdfloader/parser2v3/parse_snippet_info.go199
-rw-r--r--rdfloader/parser2v3/parse_snippet_info_test.go536
-rw-r--r--rdfloader/parser2v3/parse_spdx_document.go120
-rw-r--r--rdfloader/parser2v3/parse_spdx_document_test.go241
-rw-r--r--rdfloader/parser2v3/parser.go133
-rw-r--r--rdfloader/parser2v3/parser_test.go172
-rw-r--r--rdfloader/parser2v3/types.go127
-rw-r--r--rdfloader/parser2v3/utils.go155
-rw-r--r--rdfloader/parser2v3/utils_test.go290
-rw-r--r--rdfloader/rdfloader.go14
-rw-r--r--rdfloader/rdfloader_test.go84
-rw-r--r--reporter/reporter.go64
-rw-r--r--reporter/reporter_test.go125
-rw-r--r--spdx/common/checksum.go26
-rw-r--r--spdx/common/package.go2
-rw-r--r--spdx/v2_2/annotation.go10
-rw-r--r--spdx/v2_2/creation_info.go8
-rw-r--r--spdx/v2_2/document.go16
-rw-r--r--spdx/v2_2/file.go36
-rw-r--r--spdx/v2_2/other_license.go10
-rw-r--r--spdx/v2_2/package.go50
-rw-r--r--spdx/v2_2/relationship.go6
-rw-r--r--spdx/v2_2/review.go6
-rw-r--r--spdx/v2_2/snippet.go18
-rw-r--r--spdx/v2_3/annotation.go29
-rw-r--r--spdx/v2_3/creation_info.go26
-rw-r--r--spdx/v2_3/document.go65
-rw-r--r--spdx/v2_3/file.go96
-rw-r--r--spdx/v2_3/other_license.go31
-rw-r--r--spdx/v2_3/package.go151
-rw-r--r--spdx/v2_3/relationship.go23
-rw-r--r--spdx/v2_3/review.go25
-rw-r--r--spdx/v2_3/snippet.go48
-rw-r--r--spdxlib/described_elements.go52
-rw-r--r--spdxlib/described_elements_test.go170
-rw-r--r--spdxlib/documents.go31
-rw-r--r--spdxlib/documents_test.go87
-rw-r--r--spdxlib/relationships.go15
-rw-r--r--test/v2_3/tv_test.go491
-rw-r--r--tvloader/parser2v3/parse_annotation.go43
-rw-r--r--tvloader/parser2v3/parse_annotation_test.go158
-rw-r--r--tvloader/parser2v3/parse_creation_info.go156
-rw-r--r--tvloader/parser2v3/parse_creation_info_test.go427
-rw-r--r--tvloader/parser2v3/parse_file.go151
-rw-r--r--tvloader/parser2v3/parse_file_test.go965
-rw-r--r--tvloader/parser2v3/parse_other_license.go55
-rw-r--r--tvloader/parser2v3/parse_other_license_test.go339
-rw-r--r--tvloader/parser2v3/parse_package.go232
-rw-r--r--tvloader/parser2v3/parse_package_test.go1130
-rw-r--r--tvloader/parser2v3/parse_relationship.go54
-rw-r--r--tvloader/parser2v3/parse_relationship_test.go202
-rw-r--r--tvloader/parser2v3/parse_review.go63
-rw-r--r--tvloader/parser2v3/parse_review_test.go414
-rw-r--r--tvloader/parser2v3/parse_snippet.go137
-rw-r--r--tvloader/parser2v3/parse_snippet_test.go635
-rw-r--r--tvloader/parser2v3/parser.go100
-rw-r--r--tvloader/parser2v3/parser_test.go97
-rw-r--r--tvloader/parser2v3/types.go57
-rw-r--r--tvloader/parser2v3/util.go115
-rw-r--r--tvloader/parser2v3/util_test.go156
-rw-r--r--tvloader/tvloader.go18
-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
-rw-r--r--tvsaver/tvsaver.go9
-rw-r--r--utils/verification.go40
-rw-r--r--utils/verification_test.go165
-rw-r--r--yaml/parser.go19
-rw-r--r--yaml/writer.go16
-rw-r--r--yaml/yaml_v2_2_test.go (renamed from yaml/yaml_test.go)6
-rw-r--r--yaml/yaml_v2_3_test.go492
128 files changed, 21114 insertions, 97 deletions
diff --git a/builder/build.go b/builder/build.go
index 1594a04..3c670a0 100644
--- a/builder/build.go
+++ b/builder/build.go
@@ -9,9 +9,11 @@ import (
"github.com/spdx/tools-golang/builder/builder2v1"
"github.com/spdx/tools-golang/builder/builder2v2"
+ "github.com/spdx/tools-golang/builder/builder2v3"
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 builder =====
@@ -151,3 +153,72 @@ func Build2_2(packageName string, dirRoot string, config *Config2_2) (*v2_2.Docu
return doc, nil
}
+
+// ===== 2.3 builder =====
+
+// Config2_3 is a collection of configuration settings for builder
+// (for version 2.3 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_3.
+type Config2_3 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // CreatorType should be one of "Person", "Organization" or "Tool".
+ // If not one of those strings, it will be interpreted as "Person".
+ CreatorType string
+
+ // Creator will be filled in for the given CreatorType.
+ Creator string
+
+ // PathsIgnored lists certain paths to be omitted from the built document.
+ // Each string should be a path, relative to the package's dirRoot,
+ // to a specific file or (for all files in a directory) ending in a slash.
+ // Prefix the string with "**" to omit all instances of that file /
+ // directory, regardless of where it is in the file tree.
+ PathsIgnored []string
+
+ // TestValues is used to pass fixed values for testing purposes
+ // only, and should be set to nil for production use. It is only
+ // exported so that it will be accessible within builder2v3.
+ TestValues map[string]string
+}
+
+// Build2_3 creates an SPDX Document (version 2.3), returning that document or
+// error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - config: Config object
+func Build2_3(packageName string, dirRoot string, config *Config2_3) (*v2_3.Document, error) {
+ // build Package section first -- will include Files and make the
+ // package verification code available
+ pkg, err := builder2v3.BuildPackageSection2_3(packageName, dirRoot, config.PathsIgnored)
+ if err != nil {
+ return nil, err
+ }
+
+ ci, err := builder2v3.BuildCreationInfoSection2_3(config.CreatorType, config.Creator, config.TestValues)
+ if err != nil {
+ return nil, err
+ }
+
+ rln, err := builder2v3.BuildRelationshipSection2_3(packageName)
+ if err != nil {
+ return nil, err
+ }
+
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: packageName,
+ DocumentNamespace: fmt.Sprintf("%s%s-%s", config.NamespacePrefix, packageName, pkg.PackageVerificationCode),
+ CreationInfo: ci,
+ Packages: []*v2_3.Package{pkg},
+ Relationships: []*v2_3.Relationship{rln},
+ }
+
+ return doc, nil
+}
diff --git a/builder/builder2v3/build_creation_info.go b/builder/builder2v3/build_creation_info.go
new file mode 100644
index 0000000..04a8e16
--- /dev/null
+++ b/builder/builder2v3/build_creation_info.go
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "time"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// BuildCreationInfoSection2_3 creates an SPDX Package (version 2.3), returning that
+// package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - code: verification code from Package
+// - namespacePrefix: prefix for DocumentNamespace (packageName and code will be added)
+// - creatorType: one of Person, Organization or Tool
+// - creator: creator string
+// - testValues: for testing only; call with nil when using in production
+func BuildCreationInfoSection2_3(creatorType string, creator string, testValues map[string]string) (*v2_3.CreationInfo, error) {
+ // build creator slices
+ creators := []common.Creator{
+ // add builder as a tool
+ {
+ Creator: "github.com/spdx/tools-golang/builder",
+ CreatorType: "Tool",
+ },
+ {
+ Creator: creator,
+ CreatorType: creatorType,
+ },
+ }
+
+ // use test Created time if passing test values
+ location, _ := time.LoadLocation("UTC")
+ locationTime := time.Now().In(location)
+ created := locationTime.Format("2006-01-02T15:04:05Z")
+ if testVal := testValues["Created"]; testVal != "" {
+ created = testVal
+ }
+
+ ci := &v2_3.CreationInfo{
+ Creators: creators,
+ Created: created,
+ }
+ return ci, nil
+}
diff --git a/builder/builder2v3/build_creation_info_test.go b/builder/builder2v3/build_creation_info_test.go
new file mode 100644
index 0000000..f1868f0
--- /dev/null
+++ b/builder/builder2v3/build_creation_info_test.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+)
+
+// ===== CreationInfo section builder tests =====
+func TestBuilder2_3CanBuildCreationInfoSection(t *testing.T) {
+ creatorType := "Organization"
+ creator := "Jane Doe LLC"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "Jane Doe LLC" {
+ t.Errorf("expected %s, got %s", "Jane Doe LLC", ci.Creators[0].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[1].Creator)
+ }
+ if ci.Created != "2018-10-20T16:48:00Z" {
+ t.Errorf("expected %s, got %s", "2018-10-20T16:48:00Z", ci.Created)
+ }
+}
+
+func TestBuilder2_3CanBuildCreationInfoSectionWithCreatorPerson(t *testing.T) {
+ creatorType := "Person"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[0].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[1].Creator)
+ }
+}
+
+func TestBuilder2_3CanBuildCreationInfoSectionWithCreatorTool(t *testing.T) {
+ creatorType := "Tool"
+ creator := "some-other-tool-2.1"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+ if ci.Creators[1].Creator != "some-other-tool-2.1" {
+ t.Errorf("expected %s, got %s", "some-other-tool-2.1", ci.Creators[1])
+ }
+}
+
+func TestBuilder2_3CanBuildCreationInfoSectionWithInvalidPerson(t *testing.T) {
+ creatorType := "Whatever"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[1])
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+}
diff --git a/builder/builder2v3/build_file.go b/builder/builder2v3/build_file.go
new file mode 100644
index 0000000..bce8e66
--- /dev/null
+++ b/builder/builder2v3/build_file.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildFileSection2_3 creates an SPDX File (version 2.3), returning that
+// file or error if any is encountered. Arguments:
+// - filePath: path to file, relative to prefix
+// - prefix: relative directory for filePath
+// - fileNumber: integer index (unique within package) to use in identifier
+func BuildFileSection2_3(filePath string, prefix string, fileNumber int) (*v2_3.File, error) {
+ // build the full file path
+ p := filepath.Join(prefix, filePath)
+
+ // make sure we can get the file and its hashes
+ ssha1, ssha256, smd5, err := utils.GetHashesForFilePath(p)
+ if err != nil {
+ return nil, err
+ }
+
+ // build the identifier
+ i := fmt.Sprintf("File%d", fileNumber)
+
+ // now build the File section
+ f := &v2_3.File{
+ FileName: filePath,
+ FileSPDXIdentifier: common.ElementID(i),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: ssha1,
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: ssha256,
+ },
+ {
+ Algorithm: common.MD5,
+ Value: smd5,
+ },
+ },
+ LicenseConcluded: "NOASSERTION",
+ LicenseInfoInFiles: []string{"NOASSERTION"},
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ return f, nil
+}
diff --git a/builder/builder2v3/build_file_test.go b/builder/builder2v3/build_file_test.go
new file mode 100644
index 0000000..c3eaee6
--- /dev/null
+++ b/builder/builder2v3/build_file_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== File section builder tests =====
+func TestBuilder2_3CanBuildFileSection(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "../../testdata/project1/"
+ fileNumber := 17
+
+ file1, err := BuildFileSection2_3(filePath, prefix, fileNumber)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if file1 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file1.FileName != "/file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/file1.testdata.txt", file1.FileName)
+ }
+ if file1.FileSPDXIdentifier != common.ElementID("File17") {
+ t.Errorf("expected %v, got %v", "File17", file1.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file1.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", checksum.Value)
+ }
+ }
+ }
+
+ if file1.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseConcluded)
+ }
+ if len(file1.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file1.LicenseInfoInFiles))
+ } else {
+ if file1.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseInfoInFiles[0])
+ }
+ }
+ if file1.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.FileCopyrightText)
+ }
+
+}
+
+func TestBuilder2_3BuildFileSectionFailsForInvalidFilePath(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "oops/wrong/path"
+ fileNumber := 11
+
+ _, err := BuildFileSection2_3(filePath, prefix, fileNumber)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/builder/builder2v3/build_package.go b/builder/builder2v3/build_package.go
new file mode 100644
index 0000000..f278a59
--- /dev/null
+++ b/builder/builder2v3/build_package.go
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "runtime"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildPackageSection2_3 creates an SPDX Package (version 2.3), returning
+// that package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - pathsIgnore: slice of strings for filepaths to ignore
+func BuildPackageSection2_3(packageName string, dirRoot string, pathsIgnore []string) (*v2_3.Package, error) {
+ // build the file section first, so we'll have it available
+ // for calculating the package verification code
+ filepaths, err := utils.GetAllFilePaths(dirRoot, pathsIgnore)
+ osType := runtime.GOOS
+
+ if err != nil {
+ return nil, err
+ }
+
+ re, ok := regexp.Compile("/+")
+ if ok != nil {
+ return nil, err
+ }
+ dirRootLen := 0
+ if osType == "windows" {
+ dirRootLen = len(dirRoot)
+ }
+
+ files := []*v2_3.File{}
+ fileNumber := 0
+ for _, fp := range filepaths {
+ newFilePatch := ""
+ if osType == "windows" {
+ newFilePatch = filepath.FromSlash("." + fp[dirRootLen:])
+ } else {
+ newFilePatch = filepath.FromSlash("./" + fp)
+ }
+ newFile, err := BuildFileSection2_3(re.ReplaceAllLiteralString(newFilePatch, string(filepath.Separator)), dirRoot, fileNumber)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, newFile)
+ fileNumber++
+ }
+
+ // get the verification code
+ code, err := utils.GetVerificationCode2_3(files, "")
+ if err != nil {
+ return nil, err
+ }
+
+ // now build the package section
+ pkg := &v2_3.Package{
+ PackageName: packageName,
+ PackageSPDXIdentifier: common.ElementID(fmt.Sprintf("Package-%s", packageName)),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: &code,
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{},
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: files,
+ }
+
+ return pkg, nil
+}
diff --git a/builder/builder2v3/build_package_test.go b/builder/builder2v3/build_package_test.go
new file mode 100644
index 0000000..873e3d5
--- /dev/null
+++ b/builder/builder2v3/build_package_test.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Package section builder tests =====
+func TestBuilder2_3CanBuildPackageSection(t *testing.T) {
+ packageName := "project1"
+ dirRoot := "../../testdata/project1/"
+
+ wantVerificationCode := common.PackageVerificationCode{Value: "fc9ac4a370af0a471c2e52af66d6b4cf4e2ba12b"}
+
+ pkg, err := BuildPackageSection2_3(packageName, dirRoot, nil)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if pkg == nil {
+ t.Fatalf("expected non-nil Package, got nil")
+ }
+ if pkg.PackageName != "project1" {
+ t.Errorf("expected %v, got %v", "project1", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != common.ElementID("Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", pkg.PackageSPDXIdentifier)
+ }
+ if pkg.PackageDownloadLocation != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageDownloadLocation)
+ }
+ if pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, pkg.FilesAnalyzed)
+ }
+ if pkg.IsFilesAnalyzedTagPresent != true {
+ t.Errorf("expected %v, got %v", true, pkg.IsFilesAnalyzedTagPresent)
+ }
+ if pkg.PackageVerificationCode.Value != wantVerificationCode.Value {
+ t.Errorf("expected %v, got %v", wantVerificationCode, pkg.PackageVerificationCode)
+ }
+ if pkg.PackageLicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseConcluded)
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 0 {
+ t.Errorf("expected %v, got %v", 0, len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseDeclared != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageCopyrightText)
+ }
+
+ // and make sure we got the right number of files, and check the first one
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+ fileEmpty := pkg.Files[0]
+ if fileEmpty == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if fileEmpty.FileName != "./emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./emptyfile.testdata.txt", fileEmpty.FileName)
+ }
+ if fileEmpty.FileSPDXIdentifier != common.ElementID("File0") {
+ t.Errorf("expected %v, got %v", "File0", fileEmpty.FileSPDXIdentifier)
+ }
+ for _, checksum := range fileEmpty.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
+ t.Errorf("expected %v, got %v", "da39a3ee5e6b4b0d3255bfef95601890afd80709", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
+ t.Errorf("expected %v, got %v", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "d41d8cd98f00b204e9800998ecf8427e" {
+ t.Errorf("expected %v, got %v", "d41d8cd98f00b204e9800998ecf8427e", checksum.Value)
+ }
+ }
+ }
+ if fileEmpty.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseConcluded)
+ }
+ if len(fileEmpty.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(fileEmpty.LicenseInfoInFiles))
+ } else {
+ if fileEmpty.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseInfoInFiles[0])
+ }
+ }
+ if fileEmpty.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.FileCopyrightText)
+ }
+}
+
+func TestBuilder2_3CanIgnoreFiles(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../../testdata/project3/"
+ pathsIgnored := []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ }
+ pkg, err := BuildPackageSection2_3(packageName, dirRoot, pathsIgnored)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ // make sure we got the right files
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+
+ want := "./dontscan.txt"
+ got := pkg.Files[0].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep/keep.txt"
+ got = pkg.Files[1].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep.txt"
+ got = pkg.Files[2].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/dontscan.txt"
+ got = pkg.Files[3].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/keep.txt"
+ got = pkg.Files[4].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
diff --git a/builder/builder2v3/build_relationship.go b/builder/builder2v3/build_relationship.go
new file mode 100644
index 0000000..45ce5be
--- /dev/null
+++ b/builder/builder2v3/build_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// BuildRelationshipSection2_3 creates an SPDX Relationship (version 2.3)
+// solely for the document "DESCRIBES" package relationship, returning that
+// relationship or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+func BuildRelationshipSection2_3(packageName string) (*v2_3.Relationship, error) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", fmt.Sprintf("Package-%s", packageName)),
+ Relationship: "DESCRIBES",
+ }
+
+ return rln, nil
+}
diff --git a/builder/builder2v3/build_relationship_test.go b/builder/builder2v3/build_relationship_test.go
new file mode 100644
index 0000000..cbf27a7
--- /dev/null
+++ b/builder/builder2v3/build_relationship_test.go
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Relationship section builder tests =====
+func TestBuilder2_3CanBuildRelationshipSection(t *testing.T) {
+ packageName := "project17"
+
+ rln, err := BuildRelationshipSection2_3(packageName)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if rln == nil {
+ t.Fatalf("expected non-nil relationship, got nil")
+ }
+ if rln.RefA != common.MakeDocElementID("", "DOCUMENT") {
+ t.Errorf("expected %v, got %v", "DOCUMENT", rln.RefA)
+ }
+ if rln.RefB != common.MakeDocElementID("", "Package-project17") {
+ t.Errorf("expected %v, got %v", "Package-project17", rln.RefB)
+ }
+ if rln.Relationship != "DESCRIBES" {
+ t.Errorf("expected %v, got %v", "DESCRIBES", rln.Relationship)
+ }
+
+}
diff --git a/examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json b/examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json
new file mode 100644
index 0000000..7ac6549
--- /dev/null
+++ b/examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json
@@ -0,0 +1,406 @@
+{
+ "spdxVersion": "SPDX-2.3",
+ "dataLicense": "CC0-1.0",
+ "SPDXID": "SPDXRef-DOCUMENT",
+ "name": "SPDX-Tools-v2.0",
+ "documentNamespace": "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ "externalDocumentRefs": [
+ {
+ "externalDocumentId": "DocumentRef-spdx-tool-1.2",
+ "spdxDocument": "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ "checksum": {
+ "algorithm": "SHA1",
+ "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2759"
+ }
+ }
+ ],
+ "comment": "This document was created using SPDX 2.0 using licenses from the web site.",
+ "creationInfo": {
+ "licenseListVersion": "3.9",
+ "creators": [
+ "Tool: LicenseFind-1.0",
+ "Organization: ExampleCodeInspect ()",
+ "Person: Jane Doe ()"
+ ],
+ "created": "2010-01-29T18:30:22Z",
+ "comment": "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries."
+ },
+ "packages": [
+ {
+ "name": "glibc",
+ "SPDXID": "SPDXRef-Package",
+ "versionInfo": "2.11.1",
+ "packageFileName": "glibc-2.11.1.tar.gz",
+ "supplier": "Person: Jane Doe (jane.doe@example.com)",
+ "originator": "Organization: ExampleCodeInspect (contact@example.com)",
+ "downloadLocation": "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ "filesAnalyzed": true,
+ "packageVerificationCode": {
+ "packageVerificationCodeValue": "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ "packageVerificationCodeExcludedFiles": [
+ "./package.spdx"
+ ]
+ },
+ "checksums": [
+ {
+ "algorithm": "MD5",
+ "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+ },
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "85ed0817af83a24ad8da68c2b5094de69833983c"
+ },
+ {
+ "algorithm": "SHA256",
+ "checksumValue": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ }
+ ],
+ "homepage": "http://ftp.gnu.org/gnu/glibc",
+ "sourceInfo": "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ "licenseConcluded": "(LGPL-2.0-only OR LicenseRef-3)",
+ "licenseInfoFromFiles": [
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1"
+ ],
+ "licenseDeclared": "(LGPL-2.0-only AND LicenseRef-3)",
+ "licenseComments": "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ "copyrightText": "Copyright 2008-2010 John Smith",
+ "summary": "GNU C library.",
+ "description": "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ "externalRefs": [
+ {
+ "referenceCategory": "SECURITY",
+ "referenceType": "cpe23Type",
+ "referenceLocator": "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ "comment": ""
+ },
+ {
+ "referenceCategory": "OTHER",
+ "referenceType": "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ "referenceLocator": "acmecorp/acmenator/4.1.3-alpha",
+ "comment": "This is the external ref for Acme"
+ }
+ ],
+ "attributionTexts": [
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually."
+ ],
+ "annotations": [
+ {
+ "annotator": "Person: Package Commenter",
+ "annotationDate": "2011-01-29T18:30:22Z",
+ "annotationType": "OTHER",
+ "comment": "Package level annotation"
+ }
+ ]
+ },
+ {
+ "name": "Apache Commons Lang",
+ "SPDXID": "SPDXRef-fromDoap-1",
+ "downloadLocation": "NOASSERTION",
+ "homepage": "http://commons.apache.org/proper/commons-lang/",
+ "licenseDeclared": "NOASSERTION",
+ "copyrightText": "NOASSERTION"
+ },
+ {
+ "name": "Jena",
+ "SPDXID": "SPDXRef-fromDoap-0",
+ "versionInfo": "3.12.0",
+ "downloadLocation": "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ "homepage": "http://www.openjena.org/",
+ "licenseConcluded": "NOASSERTION",
+ "licenseDeclared": "NOASSERTION",
+ "copyrightText": "NOASSERTION",
+ "externalRefs": [
+ {
+ "referenceCategory": "PACKAGE-MANAGER",
+ "referenceType": "purl",
+ "referenceLocator": "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ "comment": ""
+ }
+ ]
+ },
+ {
+ "name": "Saxon",
+ "SPDXID": "SPDXRef-Saxon",
+ "versionInfo": "8.8",
+ "packageFileName": "saxonB-8.8.zip",
+ "downloadLocation": "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "85ed0817af83a24ad8da68c2b5094de69833983c"
+ }
+ ],
+ "homepage": "http://saxon.sourceforge.net/",
+ "licenseConcluded": "MPL-1.0",
+ "licenseDeclared": "MPL-1.0",
+ "licenseComments": "Other versions available for a commercial license",
+ "copyrightText": "Copyright Saxonica Ltd",
+ "description": "The Saxon package is a collection of tools for processing XML documents."
+ },
+ {
+ "name": "centos",
+ "SPDXID": "SPDXRef-CentOS-7",
+ "versionInfo": "centos7.9.2009",
+ "packageFileName": "saxonB-8.8.zip",
+ "downloadLocation": "NOASSERTION",
+ "homepage": "https://www.centos.org/",
+ "copyrightText": "NOASSERTION",
+ "description": "The CentOS container used to run the application.",
+ "primaryPackagePurpose": "CONTAINER",
+ "releaseDate": "2021-10-15T02:38:00Z",
+ "builtDate": "2021-09-15T02:38:00Z",
+ "validUntilDate": "2022-10-15T02:38:00Z"
+ }
+ ],
+ "files": [
+ {
+ "fileName": "./src/org/spdx/parser/DOAPProject.java",
+ "SPDXID": "SPDXRef-DoapSource",
+ "fileTypes": [
+ "SOURCE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ }
+ ],
+ "licenseConcluded": "Apache-2.0",
+ "licenseInfoInFiles": [
+ "Apache-2.0"
+ ],
+ "copyrightText": "Copyright 2010, 2011 Source Auditor Inc.",
+ "fileContributors": [
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c"
+ ]
+ },
+ {
+ "fileName": "./lib-source/commons-lang3-3.1-sources.jar",
+ "SPDXID": "SPDXRef-CommonsLangSrc",
+ "fileTypes": [
+ "ARCHIVE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "c2b4e1c67a2d28fced849ee1bb76e7391b93f125"
+ }
+ ],
+ "licenseConcluded": "Apache-2.0",
+ "licenseInfoInFiles": [
+ "Apache-2.0"
+ ],
+ "copyrightText": "Copyright 2001-2011 The Apache Software Foundation",
+ "comment": "This file is used by Jena",
+ "noticeText": "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ "fileContributors": [
+ "Apache Software Foundation"
+ ]
+ },
+ {
+ "fileName": "./lib-source/jena-2.6.3-sources.jar",
+ "SPDXID": "SPDXRef-JenaLib",
+ "fileTypes": [
+ "ARCHIVE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "3ab4e1c67a2d28fced849ee1bb76e7391b93f125"
+ }
+ ],
+ "licenseConcluded": "LicenseRef-1",
+ "licenseInfoInFiles": [
+ "LicenseRef-1"
+ ],
+ "licenseComments": "This license is used by Jena",
+ "copyrightText": "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ "comment": "This file belongs to Jena",
+ "fileContributors": [
+ "Apache Software Foundation",
+ "Hewlett Packard Inc."
+ ]
+ },
+ {
+ "fileName": "./package/foo.c",
+ "SPDXID": "SPDXRef-File",
+ "fileTypes": [
+ "SOURCE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ },
+ {
+ "algorithm": "MD5",
+ "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+ }
+ ],
+ "licenseConcluded": "(LGPL-2.0-only OR LicenseRef-2)",
+ "licenseInfoInFiles": [
+ "GPL-2.0-only",
+ "LicenseRef-2"
+ ],
+ "licenseComments": "The concluded license was taken from the package level that the file was included in.",
+ "copyrightText": "Copyright 2008-2010 John Smith",
+ "comment": "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ "noticeText": "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ "fileContributors": [
+ "The Regents of the University of California",
+ "Modified by Paul Mundt lethal@linux-sh.org",
+ "IBM Corporation"
+ ],
+ "annotations": [
+ {
+ "annotator": "Person: File Commenter",
+ "annotationDate": "2011-01-29T18:30:22Z",
+ "annotationType": "OTHER",
+ "comment": "File level annotation"
+ }
+ ]
+ }
+ ],
+ "hasExtractedLicensingInfos": [
+ {
+ "licenseId": "LicenseRef-1",
+ "extractedText": "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/"
+ },
+ {
+ "licenseId": "LicenseRef-2",
+ "extractedText": "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ },
+ {
+ "licenseId": "LicenseRef-4",
+ "extractedText": "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/"
+ },
+ {
+ "licenseId": "LicenseRef-Beerware-4.2",
+ "extractedText": "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ "name": "Beer-Ware License (Version 42)",
+ "seeAlsos": [
+ "http://people.freebsd.org/~phk/"
+ ],
+ "comment": "The beerware license has a couple of other standard variants."
+ },
+ {
+ "licenseId": "LicenseRef-3",
+ "extractedText": "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ "name": "CyberNeko License",
+ "seeAlsos": [
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com"
+ ],
+ "comment": "This is tye CyperNeko License"
+ }
+ ],
+ "relationships": [
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "SPDXRef-Package",
+ "relationshipType": "CONTAINS"
+ },
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement",
+ "relationshipType": "COPY_OF"
+ },
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "SPDXRef-File",
+ "relationshipType": "DESCRIBES"
+ },
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "SPDXRef-Package",
+ "relationshipType": "DESCRIBES"
+ },
+ {
+ "spdxElementId": "SPDXRef-Package",
+ "relatedSpdxElement": "SPDXRef-JenaLib",
+ "relationshipType": "CONTAINS"
+ },
+ {
+ "spdxElementId": "SPDXRef-Package",
+ "relatedSpdxElement": "SPDXRef-Saxon",
+ "relationshipType": "DYNAMIC_LINK"
+ },
+ {
+ "spdxElementId": "SPDXRef-CommonsLangSrc",
+ "relatedSpdxElement": "NOASSERTION",
+ "relationshipType": "GENERATED_FROM"
+ },
+ {
+ "spdxElementId": "SPDXRef-JenaLib",
+ "relatedSpdxElement": "SPDXRef-Package",
+ "relationshipType": "CONTAINS"
+ },
+ {
+ "spdxElementId": "SPDXRef-File",
+ "relatedSpdxElement": "SPDXRef-fromDoap-0",
+ "relationshipType": "GENERATED_FROM"
+ }
+ ],
+ "annotations": [
+ {
+ "annotator": "Person: Jane Doe ()",
+ "annotationDate": "2010-01-29T18:30:22Z",
+ "annotationType": "OTHER",
+ "comment": "Document level annotation"
+ },
+ {
+ "annotator": "Person: Joe Reviewer",
+ "annotationDate": "2010-02-10T00:00:00Z",
+ "annotationType": "REVIEW",
+ "comment": "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses"
+ },
+ {
+ "annotator": "Person: Suzanne Reviewer",
+ "annotationDate": "2011-03-13T00:00:00Z",
+ "annotationType": "REVIEW",
+ "comment": "Another example reviewer."
+ }
+ ],
+ "snippets": [
+ {
+ "SPDXID": "SPDXRef-Snippet",
+ "snippetFromFile": "SPDXRef-DoapSource",
+ "ranges": [
+ {
+ "startPointer": {
+ "offset": 310,
+ "reference": "SPDXRef-DoapSource"
+ },
+ "endPointer": {
+ "offset": 420,
+ "reference": "SPDXRef-DoapSource"
+ }
+ },
+ {
+ "startPointer": {
+ "lineNumber": 5,
+ "reference": "SPDXRef-DoapSource"
+ },
+ "endPointer": {
+ "lineNumber": 23,
+ "reference": "SPDXRef-DoapSource"
+ }
+ }
+ ],
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseInfoInSnippets": [
+ "GPL-2.0-only"
+ ],
+ "licenseComments": "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ "copyrightText": "Copyright 2008-2010 John Smith",
+ "comment": "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ "name": "from linux kernel"
+ }
+ ]
+} \ No newline at end of file
diff --git a/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf b/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf
index 28ba22a..df18d36 100644
--- a/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf
+++ b/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf
@@ -1143,7 +1143,7 @@ You should have received a copy of the GNU General Public License along with thi
<spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
</spdx:Review>
<spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DOCUMENT">
- <spdx:specVersion>SPDX-2.0</spdx:specVersion>
+ <spdx:specVersion>SPDX-2.2</spdx:specVersion>
<spdx:relationship>
<spdx:Relationship>
<spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
diff --git a/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx b/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx
new file mode 100644
index 0000000..7c5ae3d
--- /dev/null
+++ b/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx
@@ -0,0 +1,328 @@
+SPDXVersion: SPDX-2.3
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-SPDXRef-DOCUMENT
+DocumentName: SPDX-Tools-v2.0
+DocumentNamespace: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
+ExternalDocumentRef: DocumentRef-DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759
+DocumentComment: This document was created using SPDX 2.0 using licenses from the web site.
+LicenseListVersion: 3.9
+Creator: Tool: LicenseFind-1.0
+Creator: Organization: ExampleCodeInspect ()
+Creator: Person: Jane Doe ()
+Created: 2010-01-29T18:30:22Z
+CreatorComment: <text>This package has been shipped in source and binary form.
+The binaries were created with gcc 4.5.1 and expect to link to
+compatible system run time libraries.</text>
+
+##### Unpackaged files
+
+FileName: ./lib-source/commons-lang3-3.1-sources.jar
+SPDXID: SPDXRef-SPDXRef-CommonsLangSrc
+FileType: ARCHIVE
+FileChecksum: SHA1: c2b4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright 2001-2011 The Apache Software Foundation
+FileComment: This file is used by Jena
+FileNotice: <text>Apache Commons Lang
+Copyright 2001-2011 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software from the Spring Framework,
+under the Apache License 2.0 (see: StringUtils.containsWhitespace())</text>
+FileContributor: Apache Software Foundation
+
+FileName: ./src/org/spdx/parser/DOAPProject.java
+SPDXID: SPDXRef-SPDXRef-DoapSource
+FileType: SOURCE
+FileChecksum: SHA1: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright 2010, 2011 Source Auditor Inc.
+FileContributor: Protecode Inc.
+FileContributor: SPDX Technical Team Members
+FileContributor: Open Logic Inc.
+FileContributor: Source Auditor Inc.
+FileContributor: Black Duck Software In.c
+
+FileName: ./package/foo.c
+SPDXID: SPDXRef-SPDXRef-File
+FileType: SOURCE
+FileChecksum: SHA1: d6a770ba38583ed4bb4525bd96e50461655d2758
+FileChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+LicenseConcluded: (LGPL-2.0-only OR LicenseRef-2)
+LicenseInfoInFile: GPL-2.0-only
+LicenseInfoInFile: LicenseRef-2
+LicenseComments: The concluded license was taken from the package level that the file was included in.
+FileCopyrightText: Copyright 2008-2010 John Smith
+FileComment: <text>The concluded license was taken from the package level that the file was included in.
+This information was found in the COPYING.txt file in the xyz directory.</text>
+FileNotice: <text>Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</text>
+FileContributor: The Regents of the University of California
+FileContributor: Modified by Paul Mundt lethal@linux-sh.org
+FileContributor: IBM Corporation
+
+FileName: ./lib-source/jena-2.6.3-sources.jar
+SPDXID: SPDXRef-SPDXRef-JenaLib
+FileType: ARCHIVE
+FileChecksum: SHA1: 3ab4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: LicenseRef-1
+LicenseInfoInFile: LicenseRef-1
+LicenseComments: This license is used by Jena
+FileCopyrightText: (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+FileComment: This file belongs to Jena
+FileContributor: Apache Software Foundation
+FileContributor: Hewlett Packard Inc.
+
+##### Package: centos
+
+PackageName: centos
+SPDXID: SPDXRef-SPDXRef-CentOS-7
+PackageVersion: centos7.9.2009
+PackageFileName: saxonB-8.8.zip
+PackageDownloadLocation: NOASSERTION
+PrimaryPackagePurpose: CONTAINER
+ReleaseDate: 2021-10-15T02:38:00Z
+BuiltDate: 2021-09-15T02:38:00Z
+ValidUntilDate: 2022-10-15T02:38:00Z
+FilesAnalyzed: false
+PackageHomePage: https://www.centos.org/
+PackageCopyrightText: NOASSERTION
+PackageDescription: The CentOS container used to run the application.
+
+##### Package: glibc
+
+PackageName: glibc
+SPDXID: SPDXRef-SPDXRef-Package
+PackageVersion: 2.11.1
+PackageFileName: glibc-2.11.1.tar.gz
+PackageSupplier: Person: Jane Doe (jane.doe@example.com)
+PackageOriginator: Organization: ExampleCodeInspect (contact@example.com)
+PackageDownloadLocation: http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz
+PackageVerificationCode: d6a770ba38583ed4bb4525bd96e50461655d2758 (excludes: ./package.spdx)
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageHomePage: http://ftp.gnu.org/gnu/glibc
+PackageSourceInfo: uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.
+PackageLicenseConcluded: (LGPL-2.0-only OR LicenseRef-3)
+PackageLicenseInfoFromFiles: GPL-2.0-only
+PackageLicenseInfoFromFiles: LicenseRef-2
+PackageLicenseInfoFromFiles: LicenseRef-1
+PackageLicenseDeclared: (LGPL-2.0-only AND LicenseRef-3)
+PackageLicenseComments: The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.
+PackageCopyrightText: Copyright 2008-2010 John Smith
+PackageSummary: GNU C library.
+PackageDescription: The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.
+ExternalRef: SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*
+ExternalRef: OTHER http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge acmecorp/acmenator/4.1.3-alpha
+ExternalRefComment: This is the external ref for Acme
+PackageAttributionText: The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.
+
+##### Package: Saxon
+
+PackageName: Saxon
+SPDXID: SPDXRef-SPDXRef-Saxon
+PackageVersion: 8.8
+PackageFileName: saxonB-8.8.zip
+PackageDownloadLocation: https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download
+FilesAnalyzed: false
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageHomePage: http://saxon.sourceforge.net/
+PackageLicenseConcluded: MPL-1.0
+PackageLicenseDeclared: MPL-1.0
+PackageLicenseComments: Other versions available for a commercial license
+PackageCopyrightText: Copyright Saxonica Ltd
+PackageDescription: The Saxon package is a collection of tools for processing XML documents.
+
+##### Package: Jena
+
+PackageName: Jena
+SPDXID: SPDXRef-SPDXRef-fromDoap-0
+PackageVersion: 3.12.0
+PackageDownloadLocation: https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz
+FilesAnalyzed: false
+PackageHomePage: http://www.openjena.org/
+PackageLicenseConcluded: NOASSERTION
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: NOASSERTION
+ExternalRef: PACKAGE-MANAGER purl pkg:maven/org.apache.jena/apache-jena@3.12.0
+
+##### Package: Apache Commons Lang
+
+PackageName: Apache Commons Lang
+SPDXID: SPDXRef-SPDXRef-fromDoap-1
+PackageDownloadLocation: NOASSERTION
+FilesAnalyzed: false
+PackageHomePage: http://commons.apache.org/proper/commons-lang/
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: NOASSERTION
+
+##### Other Licenses
+
+LicenseID: LicenseRef-1
+ExtractedText: <text>/*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</text>
+
+LicenseID: LicenseRef-2
+ExtractedText: <text>This package includes the GRDDL parser developed by Hewlett Packard under the following license:
+� Copyright 2007 Hewlett-Packard Development Company, LP
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</text>
+
+LicenseID: LicenseRef-4
+ExtractedText: <text>/*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</text>
+
+LicenseID: LicenseRef-Beerware-4.2
+ExtractedText: <text>"THE BEER-WARE LICENSE" (Revision 42):
+phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp</text>
+LicenseName: Beer-Ware License (Version 42)
+LicenseCrossReference: http://people.freebsd.org/~phk/
+LicenseComment: The beerware license has a couple of other standard variants.
+
+LicenseID: LicenseRef-3
+ExtractedText: <text>The CyberNeko Software License, Version 1.0
+
+
+(C) Copyright 2002-2005, Andy Clark. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. The end-user documentation included with the redistribution,
+ if any, must include the following acknowledgment:
+ "This product includes software developed by Andy Clark."
+ Alternately, this acknowledgment may appear in the software itself,
+ if and wherever such third-party acknowledgments normally appear.
+
+4. The names "CyberNeko" and "NekoHTML" must not be used to endorse
+ or promote products derived from this software without prior
+ written permission. For written permission, please contact
+ andyc@cyberneko.net.
+
+5. Products derived from this software may not be called "CyberNeko",
+ nor may "CyberNeko" appear in their name, without prior written
+ permission of the author.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</text>
+LicenseName: CyberNeko License
+LicenseCrossReference: http://people.apache.org/~andyc/neko/LICENSE
+LicenseCrossReference: http://justasample.url.com
+LicenseComment: This is tye CyperNeko License
+
+##### Relationships
+
+Relationship: SPDXRef-DOCUMENT CONTAINS SPDXRef-Package
+Relationship: SPDXRef-DOCUMENT COPY_OF DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package
+Relationship: SPDXRef-Package CONTAINS SPDXRef-JenaLib
+Relationship: SPDXRef-Package DYNAMIC_LINK SPDXRef-Saxon
+Relationship: SPDXRef-CommonsLangSrc GENERATED_FROM NOASSERTION
+Relationship: SPDXRef-JenaLib CONTAINS SPDXRef-Package
+Relationship: SPDXRef-File GENERATED_FROM SPDXRef-fromDoap-0
+
+##### Annotations
+
+Annotator: Person: Jane Doe ()
+AnnotationDate: 2010-01-29T18:30:22Z
+AnnotationType: OTHER
+AnnotationComment: Document level annotation
+
+Annotator: Person: Joe Reviewer
+AnnotationDate: 2010-02-10T00:00:00Z
+AnnotationType: REVIEW
+AnnotationComment: This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses
+
+Annotator: Person: Suzanne Reviewer
+AnnotationDate: 2011-03-13T00:00:00Z
+AnnotationType: REVIEW
+AnnotationComment: Another example reviewer.
+
+##### Reviews
+
+Reviewer: Person: joe@example.com
+ReviewDate: 2021-11-03T05:43:21Z
+ReviewComment: This is a review comment
+
diff --git a/examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml b/examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml
new file mode 100644
index 0000000..611b74c
--- /dev/null
+++ b/examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml
@@ -0,0 +1,412 @@
+SPDXID: SPDXRef-DOCUMENT
+annotations:
+- annotationDate: "2010-01-29T18:30:22Z"
+ annotationType: OTHER
+ annotator: 'Person: Jane Doe ()'
+ comment: Document level annotation
+- annotationDate: "2010-02-10T00:00:00Z"
+ annotationType: REVIEW
+ annotator: 'Person: Joe Reviewer'
+ comment: This is just an example. Some of the non-standard licenses look like they
+ are actually BSD 3 clause licenses
+- annotationDate: "2011-03-13T00:00:00Z"
+ annotationType: REVIEW
+ annotator: 'Person: Suzanne Reviewer'
+ comment: Another example reviewer.
+comment: This document was created using SPDX 2.0 using licenses from the web site.
+creationInfo:
+ comment: |-
+ This package has been shipped in source and binary form.
+ The binaries were created with gcc 4.5.1 and expect to link to
+ compatible system run time libraries.
+ created: "2010-01-29T18:30:22Z"
+ creators:
+ - 'Tool: LicenseFind-1.0'
+ - 'Organization: ExampleCodeInspect ()'
+ - 'Person: Jane Doe ()'
+ licenseListVersion: "3.9"
+dataLicense: CC0-1.0
+documentNamespace: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
+externalDocumentRefs:
+- checksum:
+ algorithm: SHA1
+ checksumValue: d6a770ba38583ed4bb4525bd96e50461655d2759
+ externalDocumentId: DocumentRef-spdx-tool-1.2
+ spdxDocument: http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301
+files:
+- SPDXID: SPDXRef-DoapSource
+ checksums:
+ - algorithm: SHA1
+ checksumValue: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
+ copyrightText: Copyright 2010, 2011 Source Auditor Inc.
+ fileContributors:
+ - Protecode Inc.
+ - SPDX Technical Team Members
+ - Open Logic Inc.
+ - Source Auditor Inc.
+ - Black Duck Software In.c
+ fileName: ./src/org/spdx/parser/DOAPProject.java
+ fileTypes:
+ - SOURCE
+ licenseConcluded: Apache-2.0
+ licenseInfoInFiles:
+ - Apache-2.0
+- SPDXID: SPDXRef-CommonsLangSrc
+ checksums:
+ - algorithm: SHA1
+ checksumValue: c2b4e1c67a2d28fced849ee1bb76e7391b93f125
+ comment: This file is used by Jena
+ copyrightText: Copyright 2001-2011 The Apache Software Foundation
+ fileContributors:
+ - Apache Software Foundation
+ fileName: ./lib-source/commons-lang3-3.1-sources.jar
+ fileTypes:
+ - ARCHIVE
+ licenseConcluded: Apache-2.0
+ licenseInfoInFiles:
+ - Apache-2.0
+ noticeText: |-
+ Apache Commons Lang
+ Copyright 2001-2011 The Apache Software Foundation
+
+ This product includes software developed by
+ The Apache Software Foundation (http://www.apache.org/).
+
+ This product includes software from the Spring Framework,
+ under the Apache License 2.0 (see: StringUtils.containsWhitespace())
+- SPDXID: SPDXRef-JenaLib
+ checksums:
+ - algorithm: SHA1
+ checksumValue: 3ab4e1c67a2d28fced849ee1bb76e7391b93f125
+ comment: This file belongs to Jena
+ copyrightText: (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008,
+ 2009 Hewlett-Packard Development Company, LP
+ fileContributors:
+ - Apache Software Foundation
+ - Hewlett Packard Inc.
+ fileName: ./lib-source/jena-2.6.3-sources.jar
+ fileTypes:
+ - ARCHIVE
+ licenseComments: This license is used by Jena
+ licenseConcluded: LicenseRef-1
+ licenseInfoInFiles:
+ - LicenseRef-1
+- SPDXID: SPDXRef-File
+ annotations:
+ - annotationDate: "2011-01-29T18:30:22Z"
+ annotationType: OTHER
+ annotator: 'Person: File Commenter'
+ comment: File level annotation
+ checksums:
+ - algorithm: SHA1
+ checksumValue: d6a770ba38583ed4bb4525bd96e50461655d2758
+ - algorithm: MD5
+ checksumValue: 624c1abb3664f4b35547e7c73864ad24
+ comment: |-
+ The concluded license was taken from the package level that the file was included in.
+ This information was found in the COPYING.txt file in the xyz directory.
+ copyrightText: Copyright 2008-2010 John Smith
+ fileContributors:
+ - The Regents of the University of California
+ - Modified by Paul Mundt lethal@linux-sh.org
+ - IBM Corporation
+ fileName: ./package/foo.c
+ fileTypes:
+ - SOURCE
+ licenseComments: The concluded license was taken from the package level that the
+ file was included in.
+ licenseConcluded: (LGPL-2.0-only OR LicenseRef-2)
+ licenseInfoInFiles:
+ - GPL-2.0-only
+ - LicenseRef-2
+ noticeText: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is
+ hereby granted, free of charge, to any person obtaining a copy of this software
+ and associated documentation files (the �Software�), to deal in the Software without
+ restriction, including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+ persons to whom the Software is furnished to do so, subject to the following conditions:
+ \nThe above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS
+ IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ \ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE."
+hasExtractedLicensingInfos:
+- extractedText: |-
+ /*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ licenseId: LicenseRef-1
+- extractedText: "This package includes the GRDDL parser developed by Hewlett Packard
+ under the following license:\n� Copyright 2007 Hewlett-Packard Development Company,
+ LP\n\nRedistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met: \n\nRedistributions
+ of source code must retain the above copyright notice, this list of conditions
+ and the following disclaimer. \nRedistributions in binary form must reproduce
+ the above copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the distribution. \nThe
+ name of the author may not be used to endorse or promote products derived from
+ this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED
+ BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ OF THE POSSIBILITY OF SUCH DAMAGE."
+ licenseId: LicenseRef-2
+- extractedText: |-
+ /*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ licenseId: LicenseRef-4
+- comment: The beerware license has a couple of other standard variants.
+ extractedText: |-
+ "THE BEER-WARE LICENSE" (Revision 42):
+ phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+ can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp
+ licenseId: LicenseRef-Beerware-4.2
+ name: Beer-Ware License (Version 42)
+ seeAlsos:
+ - http://people.freebsd.org/~phk/
+- comment: This is tye CyperNeko License
+ extractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright
+ 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source
+ and binary forms, with or without\nmodification, are permitted provided that the
+ following conditions\nare met:\n\n1. Redistributions of source code must retain
+ the above copyright\n notice, this list of conditions and the following disclaimer.
+ \n\n2. Redistributions in binary form must reproduce the above copyright\n notice,
+ this list of conditions and the following disclaimer in\n the documentation
+ and/or other materials provided with the\n distribution.\n\n3. The end-user
+ documentation included with the redistribution,\n if any, must include the following
+ acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n
+ \ Alternately, this acknowledgment may appear in the software itself,\n if
+ and wherever such third-party acknowledgments normally appear.\n\n4. The names
+ \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products
+ derived from this software without prior \n written permission. For written
+ permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from
+ this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear
+ in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE
+ IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ licenseId: LicenseRef-3
+ name: CyberNeko License
+ seeAlsos:
+ - http://people.apache.org/~andyc/neko/LICENSE
+ - http://justasample.url.com
+name: SPDX-Tools-v2.0
+packages:
+- SPDXID: SPDXRef-Package
+ annotations:
+ - annotationDate: "2011-01-29T18:30:22Z"
+ annotationType: OTHER
+ annotator: 'Person: Package Commenter'
+ comment: Package level annotation
+ attributionTexts:
+ - The GNU C Library is free software. See the file COPYING.LIB for copying conditions,
+ and LICENSES for notices about a few contributions that require these additional
+ notices to be distributed. License copyright years may be listed using range
+ notation, e.g., 1996-2015, indicating that every year in the range, inclusive,
+ is a copyrightable year that would otherwise be listed individually.
+ checksums:
+ - algorithm: MD5
+ checksumValue: 624c1abb3664f4b35547e7c73864ad24
+ - algorithm: SHA1
+ checksumValue: 85ed0817af83a24ad8da68c2b5094de69833983c
+ - algorithm: SHA256
+ checksumValue: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+ copyrightText: Copyright 2008-2010 John Smith
+ description: The GNU C Library defines functions that are specified by the ISO C
+ standard, as well as additional features specific to POSIX and other derivatives
+ of the Unix operating system, and extensions specific to GNU systems.
+ downloadLocation: http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz
+ externalRefs:
+ - comment: ""
+ referenceCategory: SECURITY
+ referenceLocator: cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*
+ referenceType: cpe23Type
+ - comment: This is the external ref for Acme
+ referenceCategory: OTHER
+ referenceLocator: acmecorp/acmenator/4.1.3-alpha
+ referenceType: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge
+ filesAnalyzed: true
+ homepage: http://ftp.gnu.org/gnu/glibc
+ licenseComments: The license for this project changed with the release of version
+ x.y. The version of the project included here post-dates the license change.
+ licenseConcluded: (LGPL-2.0-only OR LicenseRef-3)
+ licenseDeclared: (LGPL-2.0-only AND LicenseRef-3)
+ licenseInfoFromFiles:
+ - GPL-2.0-only
+ - LicenseRef-2
+ - LicenseRef-1
+ name: glibc
+ originator: 'Organization: ExampleCodeInspect (contact@example.com)'
+ packageFileName: glibc-2.11.1.tar.gz
+ packageVerificationCode:
+ packageVerificationCodeExcludedFiles:
+ - ./package.spdx
+ packageVerificationCodeValue: d6a770ba38583ed4bb4525bd96e50461655d2758
+ sourceInfo: uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.
+ summary: GNU C library.
+ supplier: 'Person: Jane Doe (jane.doe@example.com)'
+ versionInfo: 2.11.1
+- SPDXID: SPDXRef-fromDoap-1
+ copyrightText: NOASSERTION
+ downloadLocation: NOASSERTION
+ homepage: http://commons.apache.org/proper/commons-lang/
+ licenseConcluded: NOASSERTION
+ licenseDeclared: NOASSERTION
+ name: Apache Commons Lang
+- SPDXID: SPDXRef-fromDoap-0
+ copyrightText: NOASSERTION
+ downloadLocation: https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz
+ externalRefs:
+ - comment: ""
+ referenceCategory: PACKAGE_MANAGER
+ referenceLocator: pkg:maven/org.apache.jena/apache-jena@3.12.0
+ referenceType: purl
+ homepage: http://www.openjena.org/
+ licenseConcluded: NOASSERTION
+ licenseDeclared: NOASSERTION
+ name: Jena
+ versionInfo: 3.12.0
+- SPDXID: SPDXRef-Saxon
+ checksums:
+ - algorithm: SHA1
+ checksumValue: 85ed0817af83a24ad8da68c2b5094de69833983c
+ copyrightText: Copyright Saxonica Ltd
+ description: The Saxon package is a collection of tools for processing XML documents.
+ downloadLocation: https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download
+ homepage: http://saxon.sourceforge.net/
+ licenseComments: Other versions available for a commercial license
+ licenseConcluded: MPL-1.0
+ licenseDeclared: MPL-1.0
+ name: Saxon
+ packageFileName: saxonB-8.8.zip
+ versionInfo: "8.8"
+- SPDXID: SPDXRef-CentOS-7
+ builtDate: "2021-09-15T02:38:00Z"
+ copyrightText: NOASSERTION
+ description: The CentOS container used to run the application.
+ downloadLocation: NOASSERTION
+ homepage: https://www.centos.org/
+ name: centos
+ packageFileName: saxonB-8.8.zip
+ primaryPackagePurpose: CONTAINER
+ releaseDate: "2021-10-15T02:38:00Z"
+ validUntilDate: "2022-10-15T02:38:00Z"
+ versionInfo: centos7.9.2009
+relationships:
+- comment: A relationship comment
+ relatedSpdxElement: SPDXRef-Package
+ relationshipType: CONTAINS
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement
+ relationshipType: COPY_OF
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: SPDXRef-File
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: SPDXRef-Package
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: SPDXRef-JenaLib
+ relationshipType: CONTAINS
+ spdxElementId: SPDXRef-Package
+- relatedSpdxElement: SPDXRef-Saxon
+ relationshipType: DYNAMIC_LINK
+ spdxElementId: SPDXRef-Package
+- relatedSpdxElement: NOASSERTION
+ relationshipType: GENERATED_FROM
+ spdxElementId: SPDXRef-CommonsLangSrc
+- relatedSpdxElement: SPDXRef-Package
+ relationshipType: CONTAINS
+ spdxElementId: SPDXRef-JenaLib
+- relatedSpdxElement: SPDXRef-fromDoap-0
+ relationshipType: GENERATED_FROM
+ spdxElementId: SPDXRef-File
+snippets:
+- SPDXID: SPDXRef-Snippet
+ comment: This snippet was identified as significant and highlighted in this Apache-2.0
+ file, when a commercial scanner identified it as being derived from file foo.c
+ in package xyz which is licensed under GPL-2.0.
+ copyrightText: Copyright 2008-2010 John Smith
+ licenseComments: The concluded license was taken from package xyz, from which the
+ snippet was copied into the current file. The concluded license information was
+ found in the COPYING.txt file in package xyz.
+ licenseConcluded: GPL-2.0-only
+ licenseInfoInSnippets:
+ - GPL-2.0-only
+ name: from linux kernel
+ ranges:
+ - endPointer:
+ offset: 420
+ reference: SPDXRef-DoapSource
+ startPointer:
+ offset: 310
+ reference: SPDXRef-DoapSource
+ - endPointer:
+ lineNumber: 23
+ reference: SPDXRef-DoapSource
+ startPointer:
+ lineNumber: 5
+ reference: SPDXRef-DoapSource
+ snippetFromFile: SPDXRef-DoapSource
+spdxVersion: SPDX-2.2
diff --git a/idsearcher/idsearcher.go b/idsearcher/idsearcher.go
index 29b0faa..a5176ca 100644
--- a/idsearcher/idsearcher.go
+++ b/idsearcher/idsearcher.go
@@ -7,6 +7,7 @@ package idsearcher
import (
"bufio"
"fmt"
+ "github.com/spdx/tools-golang/spdx/v2_3"
"os"
"path/filepath"
"regexp"
@@ -267,6 +268,130 @@ func BuildIDsDocument2_2(packageName string, dirRoot string, idconfig *Config2_2
return doc, nil
}
+// ===== 2.3 Searcher functions =====
+
+// Config2_3 is a collection of configuration settings for docbuilder
+// (for version 2.3 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_3.
+type Config2_3 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // BuilderPathsIgnored lists certain paths to be omitted from the built
+ // document. Each string should be a path, relative to the package's
+ // dirRoot, to a specific file or (for all files in a directory) ending
+ // in a slash. Prefix the string with "**" to omit all instances of that
+ // file / directory, regardless of where it is in the file tree.
+ BuilderPathsIgnored []string
+
+ // SearcherPathsIgnored lists certain paths that should not be searched
+ // by idsearcher, even if those paths have Files present. It uses the
+ // same format as BuilderPathsIgnored.
+ SearcherPathsIgnored []string
+}
+
+// BuildIDsDocument2_3 creates an SPDX Document (version 2.3) and searches for
+// short-form IDs in each file, filling in license fields as appropriate. It
+// returns that document or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - namespacePrefix: URI representing a prefix for the
+// namespace with which the SPDX Document will be associated
+func BuildIDsDocument2_3(packageName string, dirRoot string, idconfig *Config2_3) (*v2_3.Document, error) {
+ // first, build the Document using builder
+ bconfig := &builder.Config2_3{
+ NamespacePrefix: idconfig.NamespacePrefix,
+ CreatorType: "Tool",
+ Creator: "github.com/spdx/tools-golang/idsearcher",
+ PathsIgnored: idconfig.BuilderPathsIgnored,
+ }
+ doc, err := builder.Build2_3(packageName, dirRoot, bconfig)
+ if err != nil {
+ return nil, err
+ }
+ if doc == nil {
+ return nil, fmt.Errorf("builder returned nil Document")
+ }
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("builder returned nil Packages map")
+ }
+ if len(doc.Packages) != 1 {
+ return nil, fmt.Errorf("builder returned %d Packages", len(doc.Packages))
+ }
+
+ // now, walk through each file and find its licenses (if any)
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ return nil, fmt.Errorf("builder returned nil Package")
+ }
+ if pkg.Files == nil {
+ return nil, fmt.Errorf("builder returned nil Files in Package")
+ }
+ licsForPackage := map[string]int{}
+ for _, f := range pkg.Files {
+ // start by initializing / clearing values
+ f.LicenseInfoInFiles = []string{"NOASSERTION"}
+ f.LicenseConcluded = "NOASSERTION"
+
+ // check whether the searcher should ignore this file
+ if utils.ShouldIgnore(f.FileName, idconfig.SearcherPathsIgnored) {
+ continue
+ }
+
+ fPath := filepath.Join(dirRoot, f.FileName)
+ // FIXME this is not preferable -- ignoring error
+ ids, _ := searchFileIDs(fPath)
+ // FIXME for now, proceed onwards with whatever IDs we obtained.
+ // FIXME instead of ignoring the error, should probably either log it,
+ // FIXME and/or enable the caller to configure what should happen.
+
+ // separate out for this file's licenses
+ licsForFile := map[string]int{}
+ licsParens := []string{}
+ for _, lid := range ids {
+ // get individual elements and add for file and package
+ licElements := getIndividualLicenses(lid)
+ for _, elt := range licElements {
+ licsForFile[elt] = 1
+ licsForPackage[elt] = 1
+ }
+ // parenthesize if needed and add to slice for joining
+ licsParens = append(licsParens, makeElement(lid))
+ }
+
+ // OK -- now we can fill in the file's details, or NOASSERTION if none
+ if len(licsForFile) > 0 {
+ f.LicenseInfoInFiles = []string{}
+ for lic := range licsForFile {
+ f.LicenseInfoInFiles = append(f.LicenseInfoInFiles, lic)
+ }
+ sort.Strings(f.LicenseInfoInFiles)
+ // avoid adding parens and joining for single-ID items
+ if len(licsParens) == 1 {
+ f.LicenseConcluded = ids[0]
+ } else {
+ f.LicenseConcluded = strings.Join(licsParens, " AND ")
+ }
+ }
+ }
+
+ // and finally, we can fill in the package's details
+ if len(licsForPackage) == 0 {
+ pkg.PackageLicenseInfoFromFiles = []string{"NOASSERTION"}
+ } else {
+ pkg.PackageLicenseInfoFromFiles = []string{}
+ for lic := range licsForPackage {
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, lic)
+ }
+ sort.Strings(pkg.PackageLicenseInfoFromFiles)
+ }
+
+ return doc, nil
+}
+
// ===== Utility functions (not version-specific) =====
func searchFileIDs(filePath string) ([]string, error) {
idsMap := map[string]int{}
diff --git a/idsearcher/idsearcher_test.go b/idsearcher/idsearcher_test.go
index 00e5206..7d7a5bb 100644
--- a/idsearcher/idsearcher_test.go
+++ b/idsearcher/idsearcher_test.go
@@ -584,6 +584,295 @@ func Test2_2SearcherFailsWithInvalidPath(t *testing.T) {
}
}
+// ===== 2.3 Searcher top-level function tests =====
+func Test2_3SearcherCanFillInIDs(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "../testdata/project2/"
+ config := &Config2_3{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ }
+
+ doc, err := BuildIDsDocument2_3(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking size of each
+ if doc.Packages == nil {
+ t.Fatalf("expected non-nil Packages, got nil")
+ }
+ if len(doc.Packages) != 1 {
+ t.Fatalf("expected Packages len to be 1, got %d", len(doc.Packages))
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil Files, got nil")
+ }
+ if len(pkg.Files) != 6 {
+ t.Fatalf("expected Files len to be 6, got %d", len(pkg.Files))
+ }
+
+ fileInFolder := pkg.Files[0]
+ if fileInFolder.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileInFolder.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileInFolder.LicenseInfoInFiles))
+ }
+ if fileInFolder.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseInfoInFiles[0])
+ }
+ if fileInFolder.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseConcluded)
+ }
+
+ fileTrailingComment := pkg.Files[1]
+ if fileTrailingComment.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileTrailingComment.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileTrailingComment.LicenseInfoInFiles))
+ }
+ if fileTrailingComment.LicenseInfoInFiles[0] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseInfoInFiles[0])
+ }
+ if fileTrailingComment.LicenseConcluded != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseConcluded)
+ }
+
+ fileHasDuplicateID := pkg.Files[2]
+ if fileHasDuplicateID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasDuplicateID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileHasDuplicateID.LicenseInfoInFiles))
+ }
+ if fileHasDuplicateID.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseInfoInFiles[0])
+ }
+ if fileHasDuplicateID.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseConcluded)
+ }
+
+ fileHasID := pkg.Files[3]
+ if fileHasID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasID.LicenseInfoInFiles) != 2 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 2, got %d", len(fileHasID.LicenseInfoInFiles))
+ }
+ if fileHasID.LicenseInfoInFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", fileHasID.LicenseInfoInFiles[0])
+ }
+ if fileHasID.LicenseInfoInFiles[1] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileHasID.LicenseInfoInFiles[1])
+ }
+ if fileHasID.LicenseConcluded != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "Apache-2.0 OR GPL-2.0-or-later", fileHasID.LicenseConcluded)
+ }
+
+ fileMultipleIDs := pkg.Files[4]
+ if fileMultipleIDs.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileMultipleIDs.LicenseInfoInFiles) != 5 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 5, got %d", len(fileMultipleIDs.LicenseInfoInFiles))
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[0] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", fileMultipleIDs.LicenseInfoInFiles[0])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[1] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", fileMultipleIDs.LicenseInfoInFiles[1])
+ }
+ // here, DO NOT keep the +
+ if fileMultipleIDs.LicenseInfoInFiles[2] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", fileMultipleIDs.LicenseInfoInFiles[2])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[3] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", fileMultipleIDs.LicenseInfoInFiles[3])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[4] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileMultipleIDs.LicenseInfoInFiles[4])
+ }
+ if fileMultipleIDs.LicenseConcluded != "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+" {
+ t.Errorf("expected %v, got %v", "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+", fileMultipleIDs.LicenseConcluded)
+ }
+
+ fileNoID := pkg.Files[5]
+ if fileNoID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileNoID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileNoID.LicenseInfoInFiles))
+ }
+ if fileNoID.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseInfoInFiles[0])
+ }
+ if fileNoID.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseConcluded)
+ }
+
+ // and finally, the package should have all of these licenses
+ if pkg.PackageLicenseInfoFromFiles == nil {
+ t.Fatalf("expected non-nil PackageLicenseInfoFromFiles, got nil")
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 7 {
+ t.Fatalf("expected PackageLicenseInfoFromFiles len to be 7, got %d", len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseInfoFromFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", pkg.PackageLicenseInfoFromFiles[0])
+ }
+ if pkg.PackageLicenseInfoFromFiles[1] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", pkg.PackageLicenseInfoFromFiles[1])
+ }
+ if pkg.PackageLicenseInfoFromFiles[2] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", pkg.PackageLicenseInfoFromFiles[2])
+ }
+ // here, DO NOT keep the +
+ if pkg.PackageLicenseInfoFromFiles[3] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", pkg.PackageLicenseInfoFromFiles[3])
+ }
+ if pkg.PackageLicenseInfoFromFiles[4] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", pkg.PackageLicenseInfoFromFiles[4])
+ }
+ if pkg.PackageLicenseInfoFromFiles[5] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", pkg.PackageLicenseInfoFromFiles[5])
+ }
+ if pkg.PackageLicenseInfoFromFiles[6] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", pkg.PackageLicenseInfoFromFiles[6])
+ }
+
+}
+
+func Test2_3SearcherCanFillInIDsAndIgnorePaths(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../testdata/project3/"
+ config := &Config2_3{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ BuilderPathsIgnored: []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ },
+ SearcherPathsIgnored: []string{
+ "**/dontscan.txt",
+ },
+ }
+
+ doc, err := BuildIDsDocument2_3(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking licenses for each, and
+ // confirming NOASSERTION for those that are skipped
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected len %d, got %d", 5, len(pkg.Files))
+ }
+
+ f := pkg.Files[0]
+ if f.FileName != "./dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[1]
+ if f.FileName != "./keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[2]
+ if f.FileName != "./keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[3]
+ if f.FileName != "./subdir/keep/dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[4]
+ if f.FileName != "./subdir/keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+}
+
+func Test2_3SearcherFailsWithInvalidPath(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "./oops/invalid"
+ config := &Config2_3{
+ NamespacePrefix: "whatever",
+ }
+
+ _, err := BuildIDsDocument2_3(packageName, dirRoot, config)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
// ===== Searcher utility tests =====
func TestCanFindShortFormIDWhenPresent(t *testing.T) {
filePath := "../testdata/project2/has-id.txt"
diff --git a/json/json_test.go b/json/json_v2_2_test.go
index 2c22dcd..01400ec 100644
--- a/json/json_test.go
+++ b/json/json_v2_2_test.go
@@ -27,7 +27,7 @@ func TestLoad2_2(t *testing.T) {
}
// get a copy of the handwritten struct so we don't mutate it on accident
- handwrittenExample := want
+ handwrittenExample := want2_2
if cmp.Equal(handwrittenExample, got) {
t.Errorf("Got incorrect struct after parsing JSON example")
@@ -38,7 +38,7 @@ func TestLoad2_2(t *testing.T) {
func TestWrite2_2(t *testing.T) {
w := &bytes.Buffer{}
// get a copy of the handwritten struct so we don't mutate it on accident
- handwrittenExample := want
+ handwrittenExample := want2_2
if err := Save2_2(&handwrittenExample, w); err != nil {
t.Errorf("Save2_2() error = %v", err.Error())
return
@@ -60,7 +60,7 @@ func TestWrite2_2(t *testing.T) {
// want is handwritten translation of the official example JSON SPDX v2.2 document into a Go struct.
// We expect that the result of parsing the official document should be this value.
// We expect that the result of writing this struct should match the official example document.
-var want = v2_2.Document{
+var want2_2 = v2_2.Document{
DataLicense: "CC0-1.0",
SPDXVersion: "SPDX-2.2",
SPDXIdentifier: "SPDXRef-DOCUMENT",
diff --git a/json/json_v2_3_test.go b/json/json_v2_3_test.go
new file mode 100644
index 0000000..2cefafc
--- /dev/null
+++ b/json/json_v2_3_test.go
@@ -0,0 +1,490 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_json
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+var update = *flag.Bool("update-snapshots", false, "update the example snapshot")
+
+func TestLoad2_3(t *testing.T) {
+ fileName := "../examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json"
+
+ if update {
+ data, err := json.MarshalIndent(want2_3, "", " ")
+ if err != nil {
+ t.Errorf("unable to serialize SPDX 2.3 example to JSON: %v", err)
+ }
+ err = os.WriteFile(fileName, data, 0644)
+ if err != nil {
+ t.Errorf("unable to write SPDX 2.3 example to JSON: %v", err)
+ }
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := Load2_3(file)
+ if err != nil {
+ t.Errorf("json.parser.Load2_3() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("Got incorrect struct after parsing JSON example")
+ return
+ }
+}
+
+func TestWrite2_3(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+ if err := Save2_3(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_3() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original struct we wrote
+ parsedDoc, err := Load2_3(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("Got incorrect struct after writing and re-parsing JSON example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example JSON SPDX v2.3 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want2_3 = v2_3.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.3",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {CreatorType: "Tool", Creator: "LicenseFind-1.0"},
+ {CreatorType: "Organization", Creator: "ExampleCodeInspect ()"},
+ {CreatorType: "Person", Creator: "Jane Doe ()"},
+ },
+ Created: "2010-01-29T18:30:22Z",
+ CreatorComment: "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ },
+ DocumentComment: "This document was created using SPDX 2.0 using licenses from the web site.",
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{
+ {
+ DocumentRefID: "DocumentRef-spdx-tool-1.2",
+ URI: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ Checksum: common.Checksum{
+ Algorithm: common.SHA1,
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2759",
+ },
+ },
+ },
+ OtherLicenses: []*v2_3.OtherLicense{
+ {
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-4",
+ ExtractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-Beerware-4.2",
+ ExtractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ LicenseComment: "The beerware license has a couple of other standard variants.",
+ LicenseName: "Beer-Ware License (Version 42)",
+ LicenseCrossReferences: []string{"http://people.freebsd.org/~phk/"},
+ },
+ {
+ LicenseIdentifier: "LicenseRef-3",
+ ExtractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ LicenseName: "CyberNeko License",
+ LicenseCrossReferences: []string{
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com",
+ },
+ LicenseComment: "This is tye CyperNeko License",
+ },
+ },
+ Annotations: []*v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Jane Doe ()",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Document level annotation",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Joe Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-02-10T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Suzanne Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-03-13T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "Another example reviewer.",
+ },
+ },
+ Packages: []*v2_3.Package{
+ {
+ PackageName: "glibc",
+ PackageSPDXIdentifier: "SPDXRef-Package",
+ PackageVersion: "2.11.1",
+ PackageFileName: "glibc-2.11.1.tar.gz",
+ PackageSupplier: &common.Supplier{
+ Supplier: "Jane Doe (jane.doe@example.com)",
+ SupplierType: "Person",
+ },
+ PackageOriginator: &common.Originator{
+ Originator: "ExampleCodeInspect (contact@example.com)",
+ OriginatorType: "Organization",
+ },
+ PackageDownloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ FilesAnalyzed: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ ExcludedFiles: []string{"./package.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: "SHA256",
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ },
+ PackageHomePage: "http://ftp.gnu.org/gnu/glibc",
+ PackageSourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ PackageLicenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)",
+ PackageLicenseInfoFromFiles: []string{
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1",
+ },
+ PackageLicenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)",
+ PackageLicenseComments: "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ PackageCopyrightText: "Copyright 2008-2010 John Smith",
+ PackageSummary: "GNU C library.",
+ PackageDescription: "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ PackageComment: "",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "SECURITY",
+ RefType: "cpe23Type",
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ },
+ {
+ Category: "OTHER",
+ RefType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ Locator: "acmecorp/acmenator/4.1.3-alpha",
+ ExternalRefComment: "This is the external ref for Acme",
+ },
+ },
+ PackageAttributionTexts: []string{
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.",
+ },
+ Files: nil,
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "PACKAGE-MANAGER",
+ RefType: "purl",
+ Locator: "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ },
+ },
+ FilesAnalyzed: false,
+ PackageHomePage: "http://www.openjena.org/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageVersion: "3.12.0",
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-Saxon",
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ PackageCopyrightText: "Copyright Saxonica Ltd",
+ PackageDescription: "The Saxon package is a collection of tools for processing XML documents.",
+ PackageDownloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://saxon.sourceforge.net/",
+ PackageLicenseComments: "Other versions available for a commercial license",
+ PackageLicenseConcluded: "MPL-1.0",
+ PackageLicenseDeclared: "MPL-1.0",
+ PackageName: "Saxon",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "8.8",
+ },
+ {
+ PrimaryPackagePurpose: "CONTAINER",
+ PackageSPDXIdentifier: "SPDXRef-CentOS-7",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDescription: "The CentOS container used to run the application.",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "https://www.centos.org/",
+ PackageName: "centos",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "centos7.9.2009",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ },
+ },
+ Files: []*v2_3.File{
+ {
+ FileName: "./src/org/spdx/parser/DOAPProject.java",
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ FileTypes: []string{
+ "SOURCE",
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright 2010, 2011 Source Auditor Inc.",
+ FileContributors: []string{
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c",
+ },
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-CommonsLangSrc",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file is used by Jena",
+ FileCopyrightText: "Copyright 2001-2011 The Apache Software Foundation",
+ FileContributors: []string{"Apache Software Foundation"},
+ FileName: "./lib-source/commons-lang3-3.1-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileNotice: "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-JenaLib",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file belongs to Jena",
+ FileCopyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ FileContributors: []string{"Apache Software Foundation", "Hewlett Packard Inc."},
+ FileName: "./lib-source/jena-2.6.3-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseComments: "This license is used by Jena",
+ LicenseConcluded: "LicenseRef-1",
+ LicenseInfoInFiles: []string{"LicenseRef-1"},
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-File",
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "File Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "File level annotation",
+ },
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ },
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ FileComment: "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ FileCopyrightText: "Copyright 2008-2010 John Smith",
+ FileContributors: []string{"The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"},
+ FileName: "./package/foo.c",
+ FileTypes: []string{"SOURCE"},
+ LicenseComments: "The concluded license was taken from the package level that the file was included in.",
+ LicenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)",
+ LicenseInfoInFiles: []string{"GPL-2.0-only", "LicenseRef-2"},
+ FileNotice: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ },
+ },
+ Snippets: []v2_3.Snippet{
+ {
+ SnippetSPDXIdentifier: "SPDXRef-Snippet",
+ SnippetFromFileSPDXIdentifier: "SPDXRef-DoapSource",
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{
+ Offset: 310,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ Offset: 420,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ {
+ StartPointer: common.SnippetRangePointer{
+ LineNumber: 5,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ LineNumber: 23,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-only",
+ LicenseInfoInSnippet: []string{"GPL-2.0-only"},
+ SnippetLicenseComments: "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ SnippetCopyrightText: "Copyright 2008-2010 John Smith",
+ SnippetComment: "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ SnippetName: "from linux kernel",
+ },
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("spdx-tool-1.2", "ToolsElement"),
+ Relationship: "COPY_OF",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "JenaLib"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "Saxon"),
+ Relationship: "DYNAMIC_LINK",
+ },
+ {
+ RefA: common.MakeDocElementID("", "CommonsLangSrc"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "GENERATED_FROM",
+ },
+ {
+ RefA: common.MakeDocElementID("", "JenaLib"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "File"),
+ RefB: common.MakeDocElementID("", "fromDoap-0"),
+ Relationship: "GENERATED_FROM",
+ },
+ },
+ Reviews: []*v2_3.Review{
+ {
+ Reviewer: "joe@example.com",
+ ReviewerType: "Person",
+ ReviewDate: "2021-11-03T05:43:21Z",
+ ReviewComment: "This is a review comment",
+ },
+ },
+}
diff --git a/json/parser.go b/json/parser.go
index 5c9e6f4..ee7915d 100644
--- a/json/parser.go
+++ b/json/parser.go
@@ -8,6 +8,7 @@ import (
"io"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// Load2_2 takes in an io.Reader and returns an SPDX document.
@@ -27,3 +28,21 @@ func Load2_2(content io.Reader) (*v2_2.Document, error) {
return &doc, nil
}
+
+// Load2_3 takes in an io.Reader and returns an SPDX document.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ // convert io.Reader to a slice of bytes and call the parser
+ buf := new(bytes.Buffer)
+ _, err := buf.ReadFrom(content)
+ if err != nil {
+ return nil, err
+ }
+
+ var doc v2_3.Document
+ err = json.Unmarshal(buf.Bytes(), &doc)
+ if err != nil {
+ return nil, err
+ }
+
+ return &doc, nil
+}
diff --git a/json/writer.go b/json/writer.go
index bcc00a7..8f2b94d 100644
--- a/json/writer.go
+++ b/json/writer.go
@@ -4,6 +4,7 @@ package spdx_json
import (
"encoding/json"
+ "github.com/spdx/tools-golang/spdx/v2_3"
"io"
"github.com/spdx/tools-golang/spdx/v2_2"
@@ -23,3 +24,18 @@ func Save2_2(doc *v2_2.Document, w io.Writer) error {
return nil
}
+
+// Save2_3 takes an SPDX Document (version 2.2) and an io.Writer, and writes the document to the writer in JSON format.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ buf, err := json.Marshal(doc)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/licensediff/licensediff.go b/licensediff/licensediff.go
index 98da0e5..ba8cd84 100644
--- a/licensediff/licensediff.go
+++ b/licensediff/licensediff.go
@@ -6,6 +6,7 @@ package licensediff
import (
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// LicensePair is a result set where we are talking about two license strings,
@@ -71,6 +72,34 @@ func MakePairs2_2(p1 *v2_2.Package, p2 *v2_2.Package) (map[string]LicensePair, e
return pairs, nil
}
+// MakePairs2_3 essentially just consolidates all files and LicenseConcluded
+// strings into a single data structure.
+func MakePairs2_3(p1 *v2_3.Package, p2 *v2_3.Package) (map[string]LicensePair, error) {
+ pairs := map[string]LicensePair{}
+
+ // first, go through and add all files/licenses from p1
+ for _, f := range p1.Files {
+ pair := LicensePair{First: f.LicenseConcluded, Second: ""}
+ pairs[f.FileName] = pair
+ }
+
+ // now, go through all files/licenses from p2. If already
+ // present, add as .second; if not, create new pair
+ for _, f := range p2.Files {
+ firstLic := ""
+ existingPair, ok := pairs[f.FileName]
+ if ok {
+ // already present; update it
+ firstLic = existingPair.First
+ }
+ // now, update what's there, either way
+ pair := LicensePair{First: firstLic, Second: f.LicenseConcluded}
+ pairs[f.FileName] = pair
+ }
+
+ return pairs, nil
+}
+
// LicenseDiff is a structured version of the output of MakePairs. It is
// meant to make it easier to find and report on, e.g., just the files that
// have different licenses, or those that are in just one scan.
diff --git a/licensediff/licensediff_test.go b/licensediff/licensediff_test.go
index 1992cce..226ad51 100644
--- a/licensediff/licensediff_test.go
+++ b/licensediff/licensediff_test.go
@@ -8,6 +8,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 License diff top-level function tests =====
@@ -1049,3 +1050,557 @@ func Test2_2DifferCanCreateDiffStructuredResults(t *testing.T) {
}
}
+
+// ===== 2.3 License diff top-level function tests =====
+func Test2_3DifferCanCreateDiffPairs(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_3.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_3.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_3.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_3.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_3(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are what we expect
+ // there should be 6 entries, one for each unique filename
+ if len(diffMap) != 6 {
+ t.Fatalf("Expected %d, got %d", 6, len(diffMap))
+ }
+
+ // check each filename is present, and check its pair
+ // pair 1 -- same in both
+ pair1, ok := diffMap["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair1")
+ }
+ if pair1.First != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, pair1.First)
+ }
+ if pair1.Second != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair1.Second)
+ }
+
+ // pair 2 -- only in first
+ pair2, ok := diffMap["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair2")
+ }
+ if pair2.First != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair2.First)
+ }
+ if pair2.Second != "" {
+ t.Errorf("Expected %s, got %s", "", pair2.Second)
+ }
+
+ // pair 3 -- only in second
+ pair3, ok := diffMap["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair3")
+ }
+ if pair3.First != "" {
+ t.Errorf("Expected %s, got %s", "", pair3.First)
+ }
+ if pair3.Second != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, pair3.Second)
+ }
+
+ // pair 4 -- in both but different license
+ pair4, ok := diffMap["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair4")
+ }
+ if pair4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, pair4.First)
+ }
+ if pair4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, pair4.Second)
+ }
+
+ // pair 5 -- in both but different hash, same license
+ pair5, ok := diffMap["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair5")
+ }
+ if pair5.First != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, pair5.First)
+ }
+ if pair5.Second != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, pair5.Second)
+ }
+
+ // pair 6 -- in both but different hash, different license
+ pair6, ok := diffMap["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair6")
+ }
+ if pair6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, pair6.First)
+ }
+ if pair6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, pair6.Second)
+ }
+}
+
+func Test2_3DifferCanCreateDiffStructuredResults(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_3.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_3.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_3.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_3.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_3(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // now, create the LicenseDiff structured results from the pairs
+ diffResults, err := MakeResults(diffMap)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are the expected lengths
+ if len(diffResults.InBothChanged) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothChanged))
+ }
+ if len(diffResults.InBothSame) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothSame))
+ }
+ if len(diffResults.InFirstOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InFirstOnly))
+ }
+ if len(diffResults.InSecondOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InSecondOnly))
+ }
+
+ // check each filename is present where it belongs, and check license(s)
+
+ // in both and different license: f4 and f6
+ // filename will map to a LicensePair
+ check4, ok := diffResults.InBothChanged["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check4")
+ }
+ if check4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, check4.First)
+ }
+ if check4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, check4.Second)
+ }
+ check6, ok := diffResults.InBothChanged["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check6")
+ }
+ if check6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, check6.First)
+ }
+ if check6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, check6.Second)
+ }
+
+ // in both and same license: f1 and f5
+ // filename will map to a string
+ check1, ok := diffResults.InBothSame["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check1")
+ }
+ if check1 != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, check1)
+ }
+ check5, ok := diffResults.InBothSame["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check5")
+ }
+ if check5 != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, check5)
+ }
+ if check5 != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, check5)
+ }
+
+ // in first only: f2
+ // filename will map to a string
+ check2, ok := diffResults.InFirstOnly["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check2")
+ }
+ if check2 != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, check2)
+ }
+
+ // in second only: f3
+ // filename will map to a string
+ check3, ok := diffResults.InSecondOnly["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check3")
+ }
+ if check3 != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, check2)
+ }
+
+}
diff --git a/rdfloader/parser2v3/constants.go b/rdfloader/parser2v3/constants.go
new file mode 100644
index 0000000..44ca0d4
--- /dev/null
+++ b/rdfloader/parser2v3/constants.go
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import "github.com/spdx/gordf/rdfloader/parser"
+
+var (
+ // NAMESPACES
+ NS_SPDX = "http://spdx.org/rdf/terms#"
+ NS_RDFS = "http://www.w3.org/2000/01/rdf-schema#"
+ NS_RDF = parser.RDFNS
+ NS_PTR = "http://www.w3.org/2009/pointers#"
+ NS_DOAP = "http://usefulinc.com/ns/doap#"
+
+ // SPDX properties
+ SPDX_SPEC_VERSION = NS_SPDX + "specVersion"
+ SPDX_DATA_LICENSE = NS_SPDX + "dataLicense"
+ SPDX_NAME = NS_SPDX + "name"
+ SPDX_EXTERNAL_DOCUMENT_REF = NS_SPDX + "externalDocumentRef"
+ SPDX_LICENSE_LIST_VERSION = NS_SPDX + "licenseListVersion"
+ SPDX_CREATOR = NS_SPDX + "creator"
+ SPDX_CREATED = NS_SPDX + "created"
+ SPDX_REVIEWED = NS_SPDX + "reviewed"
+ SPDX_DESCRIBES_PACKAGE = NS_SPDX + "describesPackage"
+ SPDX_HAS_EXTRACTED_LICENSING_INFO = NS_SPDX + "hasExtractedLicensingInfo"
+ SPDX_RELATIONSHIP = NS_SPDX + "relationship"
+ SPDX_ANNOTATION = NS_SPDX + "annotation"
+ SPDX_COMMENT = NS_SPDX + "comment"
+ SPDX_CREATION_INFO = NS_SPDX + "creationInfo"
+ SPDX_CHECKSUM_ALGORITHM_SHA1 = NS_SPDX + "checksumAlgorithm_sha1"
+ SPDX_CHECKSUM_ALGORITHM_SHA256 = NS_SPDX + "checksumAlgorithm_sha256"
+ SPDX_CHECKSUM_ALGORITHM_MD5 = NS_SPDX + "checksumAlgorithm_md5"
+ SPDX_EXTERNAL_DOCUMENT_ID = NS_SPDX + "externalDocumentId"
+ SPDX_SPDX_DOCUMENT = NS_SPDX + "spdxDocument"
+ SPDX_SPDX_DOCUMENT_CAPITALIZED = NS_SPDX + "SpdxDocument"
+ SPDX_CHECKSUM = NS_SPDX + "checksum"
+ SPDX_CHECKSUM_CAPITALIZED = NS_SPDX + "Checksum"
+ SPDX_ANNOTATION_TYPE = NS_SPDX + "annotationType"
+ SPDX_ANNOTATION_TYPE_OTHER = NS_SPDX + "annotationType_other"
+ SPDX_ANNOTATION_TYPE_REVIEW = NS_SPDX + "annotationType_review"
+ SPDX_LICENSE_INFO_IN_FILE = NS_SPDX + "licenseInfoInFile"
+ SPDX_LICENSE_CONCLUDED = NS_SPDX + "licenseConcluded"
+ SPDX_LICENSE_COMMENTS = NS_SPDX + "licenseComments"
+ SPDX_COPYRIGHT_TEXT = NS_SPDX + "copyrightText"
+ SPDX_ARTIFACT_OF = NS_SPDX + "artifactOf"
+ SPDX_NOTICE_TEXT = NS_SPDX + "noticeText"
+ SPDX_FILE_CONTRIBUTOR = NS_SPDX + "fileContributor"
+ SPDX_FILE_DEPENDENCY = NS_SPDX + "fileDependency"
+ SPDX_FILE_TYPE = NS_SPDX + "fileType"
+ SPDX_FILE_NAME = NS_SPDX + "fileName"
+ SPDX_EXTRACTED_TEXT = NS_SPDX + "extractedText"
+ SPDX_LICENSE_ID = NS_SPDX + "licenseId"
+ SPDX_FILE = NS_SPDX + "File"
+ SPDX_PACKAGE = NS_SPDX + "Package"
+ SPDX_SPDX_ELEMENT = NS_SPDX + "SpdxElement"
+ SPDX_VERSION_INFO = NS_SPDX + "versionInfo"
+ SPDX_PACKAGE_FILE_NAME = NS_SPDX + "packageFileName"
+ SPDX_SUPPLIER = NS_SPDX + "supplier"
+ SPDX_ORIGINATOR = NS_SPDX + "originator"
+ SPDX_DOWNLOAD_LOCATION = NS_SPDX + "downloadLocation"
+ SPDX_FILES_ANALYZED = NS_SPDX + "filesAnalyzed"
+ SPDX_PACKAGE_VERIFICATION_CODE = NS_SPDX + "packageVerificationCode"
+ SPDX_SOURCE_INFO = NS_SPDX + "sourceInfo"
+ SPDX_LICENSE_INFO_FROM_FILES = NS_SPDX + "licenseInfoFromFiles"
+ SPDX_LICENSE_DECLARED = NS_SPDX + "licenseDeclared"
+ SPDX_SUMMARY = NS_SPDX + "summary"
+ SPDX_DESCRIPTION = NS_SPDX + "description"
+ SPDX_EXTERNAL_REF = NS_SPDX + "externalRef"
+ SPDX_HAS_FILE = NS_SPDX + "hasFile"
+ SPDX_PRIMARY_PACKAGE_PURPOSE = NS_SPDX + "primaryPackagePurpose"
+ SPDX_RELEASE_DATE = NS_SPDX + "releaseDate"
+ SPDX_BUILT_DATE = NS_SPDX + "builtDate"
+ SPDX_VALID_UNTIL_DATE = NS_SPDX + "validUntilDate"
+ SPDX_ATTRIBUTION_TEXT = NS_SPDX + "attributionText"
+ SPDX_PACKAGE_VERIFICATION_CODE_VALUE = NS_SPDX + "packageVerificationCodeValue"
+ SPDX_PACKAGE_VERIFICATION_CODE_EXCLUDED_FILE = NS_SPDX + "packageVerificationCodeExcludedFile"
+ SPDX_RELATED_SPDX_ELEMENT = NS_SPDX + "relatedSpdxElement"
+ SPDX_RELATIONSHIP_TYPE = NS_SPDX + "relationshipType"
+ SPDX_SNIPPET_FROM_FILE = NS_SPDX + "snippetFromFile"
+ SPDX_LICENSE_INFO_IN_SNIPPET = NS_SPDX + "licenseInfoInSnippet"
+ SPDX_RANGE = NS_SPDX + "range"
+ SPDX_REVIEWER = NS_SPDX + "reviewer"
+ SPDX_REVIEW_DATE = NS_SPDX + "reviewDate"
+ SPDX_SNIPPET = NS_SPDX + "Snippet"
+ SPDX_ALGORITHM = NS_SPDX + "algorithm"
+ SPDX_CHECKSUM_VALUE = NS_SPDX + "checksumValue"
+ SPDX_REFERENCE_CATEGORY = NS_SPDX + "referenceCategory"
+ SPDX_REFERENCE_CATEGORY_PACKAGE_MANAGER = NS_SPDX + "referenceCategory_packageManager"
+ SPDX_REFERENCE_CATEGORY_SECURITY = NS_SPDX + "referenceCategory_security"
+ SPDX_REFERENCE_CATEGORY_OTHER = NS_SPDX + "referenceCategory_other"
+
+ SPDX_REFERENCE_TYPE = NS_SPDX + "referenceType"
+ SPDX_REFERENCE_LOCATOR = NS_SPDX + "referenceLocator"
+ SPDX_ANNOTATION_DATE = NS_SPDX + "annotationDate"
+ SPDX_ANNOTATOR = NS_SPDX + "annotator"
+ SPDX_MEMBER = NS_SPDX + "member"
+ SPDX_DISJUNCTIVE_LICENSE_SET = NS_SPDX + "DisjunctiveLicenseSet"
+ SPDX_CONJUNCTIVE_LICENSE_SET = NS_SPDX + "ConjunctiveLicenseSet"
+ SPDX_EXTRACTED_LICENSING_INFO = NS_SPDX + "ExtractedLicensingInfo"
+ SPDX_SIMPLE_LICENSING_INFO = NS_SPDX + "SimpleLicensingInfo"
+ SPDX_NONE_CAPS = NS_SPDX + "NONE"
+ SPDX_NOASSERTION_CAPS = NS_SPDX + "NOASSERTION"
+ SPDX_NONE_SMALL = NS_SPDX + "none"
+ SPDX_NOASSERTION_SMALL = NS_SPDX + "noassertion"
+ SPDX_LICENSE = NS_SPDX + "License"
+ SPDX_LISTED_LICENSE = NS_SPDX + "ListedLicense"
+ SPDX_EXAMPLE = NS_SPDX + "example"
+ SPDX_IS_OSI_APPROVED = NS_SPDX + "isOsiApproved"
+ SPDX_STANDARD_LICENSE_TEMPLATE = NS_SPDX + "standardLicenseTemplate"
+ SPDX_IS_DEPRECATED_LICENSE_ID = NS_SPDX + "isDeprecatedLicenseId"
+ SPDX_IS_FSF_LIBRE = NS_SPDX + "isFsfLibre"
+ SPDX_LICENSE_TEXT = NS_SPDX + "licenseText"
+ SPDX_STANDARD_LICENSE_HEADER = NS_SPDX + "standardLicenseHeader"
+ SPDX_LICENSE_EXCEPTION_ID = NS_SPDX + "licenseExceptionId"
+ SPDX_LICENSE_EXCEPTION_TEXT = NS_SPDX + "licenseExceptionText"
+ SPDX_LICENSE_EXCEPTION = NS_SPDX + "licenseException"
+ SPDX_WITH_EXCEPTION_OPERATOR = NS_SPDX + "WithExceptionOperator"
+ SPDX_OR_LATER_OPERATOR = NS_SPDX + "OrLaterOperator"
+ SPDX_STANDARD_LICENSE_HEADER_TEMPLATE = NS_SPDX + "standardLicenseHeaderTemplate"
+
+ // RDFS properties
+ RDFS_COMMENT = NS_RDFS + "comment"
+ RDFS_SEE_ALSO = NS_RDFS + "seeAlso"
+
+ // RDF properties
+ RDF_TYPE = NS_RDF + "type"
+
+ // DOAP properties
+ DOAP_HOMEPAGE = NS_DOAP + "homepage"
+ DOAP_NAME = NS_DOAP + "name"
+
+ // PTR properties
+ PTR_START_END_POINTER = NS_PTR + "StartEndPointer"
+ PTR_START_POINTER = NS_PTR + "startPointer"
+ PTR_BYTE_OFFSET_POINTER = NS_PTR + "ByteOffsetPointer"
+ PTR_LINE_CHAR_POINTER = NS_PTR + "LineCharPointer"
+ PTR_REFERENCE = NS_PTR + "reference"
+ PTR_OFFSET = NS_PTR + "offset"
+ PTR_LINE_NUMBER = NS_PTR + "lineNumber"
+ PTR_END_POINTER = NS_PTR + "endPointer"
+
+ // prefixes
+ PREFIX_RELATIONSHIP_TYPE = "relationshipType_"
+)
+
+func AllRelationshipTypes() []string {
+ return []string{
+ "amendment", "ancestorOf", "buildDependencyOf", "buildToolOf",
+ "containedBy", "contains", "copyOf", "dataFile", "dataFileOf",
+ "dependencyManifestOf", "dependencyOf", "dependsOn", "descendantOf",
+ "describedBy", "describes", "devDependencyOf", "devToolOf",
+ "distributionArtifact", "documentation", "dynamicLink", "exampleOf",
+ "expandedFromArchive", "fileAdded", "fileDeleted", "fileModified",
+ "generatedFrom", "generates", "hasPrerequisite", "metafileOf",
+ "optionalComponentOf", "optionalDependencyOf", "other", "packageOf",
+ "patchApplied", "patchFor", "prerequisiteFor", "providedDependencyOf",
+ "runtimeDependencyOf", "staticLink", "testDependencyOf", "testOf",
+ "testToolOf", "testcaseOf", "variantOf",
+ }
+}
+
+func AllStandardLicenseIDS() []string {
+ return []string{
+ "0BSD", "389-exception", "AAL", "Abstyles", "Adobe-2006", "Adobe-Glyph",
+ "ADSL", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "Afmparse",
+ "AGPL-1.0-only", "AGPL-1.0-or-later", "AGPL-1.0", "AGPL-3.0-only",
+ "AGPL-3.0-or-later", "AGPL-3.0", "Aladdin", "AMDPLPA", "AML", "AMPAS",
+ "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APAFML", "APL-1.0",
+ "APSL-1.0", "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0-cl8",
+ "Artistic-1.0-Perl", "Artistic-1.0", "Artistic-2.0", "",
+ "Autoconf-exception-2.0", "Autoconf-exception-3.0", "Bahyph", "Barr",
+ "Beerware", "Bison-exception-2.2", "BitTorrent-1.0", "BitTorrent-1.1",
+ "blessing", "BlueOak-1.0.0", "Bootloader-exception", "Borceux", "BSD-1-Clause",
+ "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", "BSD-2-Clause-Patent",
+ "BSD-2-Clause-Views", "BSD-2-Clause", "BSD-3-Clause-Attribution",
+ "BSD-3-Clause-Clear", "BSD-3-Clause-LBNL",
+ "BSD-3-Clause-No-Nuclear-License-2014", "BSD-3-Clause-No-Nuclear-License",
+ "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause-Open-MPI", "BSD-3-Clause",
+ "BSD-4-Clause-UC", "BSD-4-Clause", "BSD-Protection", "BSD-Source-Code",
+ "BSL-1.0", "bzip2-1.0.5", "bzip2-1.0.6", "CAL-1.0-Combined-Work-Exception",
+ "CAL-1.0", "Caldera", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5",
+ "CC-BY-3.0-AT", "CC-BY-3.0", "CC-BY-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0",
+ "CC-BY-NC-2.5", "CC-BY-NC-3.0", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0",
+ "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0-IGO", "CC-BY-NC-ND-3.0",
+ "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5",
+ "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0",
+ "CC-BY-ND-2.5", "CC-BY-ND-3.0", "CC-BY-ND-4.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0",
+ "CC-BY-SA-2.5", "CC-BY-SA-3.0-AT", "CC-BY-SA-3.0", "CC-BY-SA-4.0", "CC-PDDC",
+ "CC0-1.0", "CDDL-1.0", "CDDL-1.1", "CDLA-Permissive-1.0", "CDLA-Sharing-1.0",
+ "CECILL-1.0", "CECILL-1.1", "CECILL-2.0", "CECILL-2.1", "CECILL-B", "CECILL-C",
+ "CERN-OHL-1.1", "CERN-OHL-1.2", "CERN-OHL-P-2.0", "CERN-OHL-S-2.0",
+ "CERN-OHL-W-2.0", "ClArtistic", "Classpath-exception-2.0",
+ "CLISP-exception-2.0", "CNRI-Jython", "CNRI-Python-GPL-Compatible",
+ "CNRI-Python", "Condor-1.1", "copyleft-next-0.3.0", "copyleft-next-0.3.1",
+ "CPAL-1.0", "CPL-1.0", "CPOL-1.02", "Crossword", "CrystalStacker",
+ "CUA-OPL-1.0", "Cube", "curl", "D-FSL-1.0", "diffmark",
+ "DigiRule-FOSS-exception", "DOC", "Dotseqn", "DSDP", "dvipdfm", "ECL-1.0",
+ "ECL-2.0", "eCos-2.0", "eCos-exception-2.0", "EFL-1.0", "EFL-2.0", "eGenix",
+ "Entessa", "EPICS", "EPL-1.0", "EPL-2.0", "ErlPL-1.1", "etalab-2.0",
+ "EUDatagrid", "EUPL-1.0", "EUPL-1.1", "EUPL-1.2", "Eurosym", "Fair",
+ "Fawkes-Runtime-exception", "FLTK-exception", "Font-exception-2.0",
+ "Frameworx-1.0", "FreeImage", "freertos-exception-2.0", "FSFAP", "FSFUL",
+ "FSFULLR", "FTL", "GCC-exception-2.0", "GCC-exception-3.1",
+ "GFDL-1.1-invariants-only", "GFDL-1.1-invariants-or-later",
+ "GFDL-1.1-no-invariants-only", "GFDL-1.1-no-invariants-or-later",
+ "GFDL-1.1-only", "GFDL-1.1-or-later", "GFDL-1.1", "GFDL-1.2-invariants-only",
+ "GFDL-1.2-invariants-or-later", "GFDL-1.2-no-invariants-only",
+ "GFDL-1.2-no-invariants-or-later", "GFDL-1.2-only", "GFDL-1.2-or-later",
+ "GFDL-1.2", "GFDL-1.3-invariants-only", "GFDL-1.3-invariants-or-later",
+ "GFDL-1.3-no-invariants-only", "GFDL-1.3-no-invariants-or-later",
+ "GFDL-1.3-only", "GFDL-1.3-or-later", "GFDL-1.3", "Giftware", "GL2PS", "Glide",
+ "Glulxe", "GLWTPL", "gnu-javamail-exception", "gnuplot", "GPL-1.0+",
+ "GPL-1.0-only", "GPL-1.0-or-later", "GPL-1.0", "GPL-2.0+", "GPL-2.0-only",
+ "GPL-2.0-or-later", "GPL-2.0-with-autoconf-exception",
+ "GPL-2.0-with-bison-exception", "GPL-2.0-with-classpath-exception",
+ "GPL-2.0-with-font-exception", "GPL-2.0-with-GCC-exception", "GPL-2.0",
+ "GPL-3.0+", "GPL-3.0-linking-exception", "GPL-3.0-linking-source-exception",
+ "GPL-3.0-only", "GPL-3.0-or-later", "GPL-3.0-with-autoconf-exception",
+ "GPL-3.0-with-GCC-exception", "GPL-3.0", "GPL-CC-1.0", "gSOAP-1.3b",
+ "HaskellReport", "Hippocratic-2.1", "HPND-sell-variant", "HPND",
+ "i2p-gpl-java-exception", "IBM-pibs", "ICU", "IJG", "ImageMagick", "iMatix",
+ "Imlib2", "Info-ZIP", "Intel-ACPI", "Intel", "Interbase-1.0", "IPA", "IPL-1.0",
+ "ISC", "JasPer-2.0", "JPNIC", "JSON", "LAL-1.2", "LAL-1.3", "Latex2e",
+ "Leptonica", "LGPL-2.0+", "LGPL-2.0-only", "LGPL-2.0-or-later", "LGPL-2.0",
+ "LGPL-2.1+", "LGPL-2.1-only", "LGPL-2.1-or-later", "LGPL-2.1", "LGPL-3.0+",
+ "LGPL-3.0-linking-exception", "LGPL-3.0-only", "LGPL-3.0-or-later", "LGPL-3.0",
+ "LGPLLR", "libpng-2.0", "Libpng", "libselinux-1.0", "libtiff",
+ "Libtool-exception", "licenses", "LiLiQ-P-1.1", "LiLiQ-R-1.1",
+ "LiLiQ-Rplus-1.1", "Linux-OpenIB", "Linux-syscall-note", "LLVM-exception",
+ "LPL-1.0", "LPL-1.02", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3a",
+ "LPPL-1.3c", "LZMA-exception", "MakeIndex", "mif-exception", "MirOS", "MIT-0",
+ "MIT-advertising", "MIT-CMU", "MIT-enna", "MIT-feh", "MIT", "MITNFA",
+ "Motosoto", "mpich2", "MPL-1.0", "MPL-1.1", "MPL-2.0-no-copyleft-exception",
+ "MPL-2.0", "MS-PL", "MS-RL", "MTLL", "MulanPSL-1.0", "MulanPSL-2.0", "Multics",
+ "Mup", "NASA-1.3", "Naumen", "NBPL-1.0", "NCGL-UK-2.0", "NCSA", "Net-SNMP",
+ "NetCDF", "Newsletr", "NGPL", "NIST-PD-fallback", "NIST-PD", "NLOD-1.0",
+ "NLPL", "Nokia-Qt-exception-1.1", "Nokia", "NOSL", "Noweb", "NPL-1.0",
+ "NPL-1.1", "NPOSL-3.0", "NRL", "NTP-0", "NTP", "Nunit", "O-UDA-1.0",
+ "OCaml-LGPL-linking-exception", "OCCT-exception-1.0", "OCCT-PL", "OCLC-2.0",
+ "ODbL-1.0", "ODC-By-1.0", "OFL-1.0-no-RFN", "OFL-1.0-RFN", "OFL-1.0",
+ "OFL-1.1-no-RFN", "OFL-1.1-RFN", "OFL-1.1", "OGC-1.0", "OGL-Canada-2.0",
+ "OGL-UK-1.0", "OGL-UK-2.0", "OGL-UK-3.0", "OGTSL", "OLDAP-1.1", "OLDAP-1.2",
+ "OLDAP-1.3", "OLDAP-1.4", "OLDAP-2.0.1", "OLDAP-2.0", "OLDAP-2.1",
+ "OLDAP-2.2.1", "OLDAP-2.2.2", "OLDAP-2.2", "OLDAP-2.3", "OLDAP-2.4",
+ "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OLDAP-2.8", "OML", "",
+ "OpenJDK-assembly-exception-1.0", "OpenSSL", "openvpn-openssl-exception",
+ "OPL-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1",
+ "OSL-3.0", "Parity-6.0.0", "Parity-7.0.0", "PDDL-1.0", "PHP-3.0", "PHP-3.01",
+ "Plexus", "PolyForm-Noncommercial-1.0.0", "PolyForm-Small-Business-1.0.0",
+ "PostgreSQL", "PS-or-PDF-font-exception-20170817", "PSF-2.0", "psfrag",
+ "psutils", "Python-2.0", "Qhull", "QPL-1.0", "Qt-GPL-exception-1.0",
+ "Qt-LGPL-exception-1.1", "Qwt-exception-1.0", "Rdisc", "RHeCos-1.1", "RPL-1.1",
+ "RPL-1.5", "RPSL-1.0", "RSA-MD", "RSCPL", "Ruby", "SAX-PD", "Saxpath", "SCEA",
+ "Sendmail-8.23", "Sendmail", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", "SHL-0.5",
+ "SHL-0.51", "SHL-2.0", "SHL-2.1", "SimPL-2.0", "SISSL-1.2", "SISSL",
+ "Sleepycat", "SMLNJ", "SMPPL", "SNIA", "Spencer-86", "Spencer-94",
+ "Spencer-99", "SPL-1.0", "SSH-OpenSSH", "SSH-short", "SSPL-1.0",
+ "StandardML-NJ", "SugarCRM-1.1.3", "Swift-exception", "SWL", "TAPR-OHL-1.0",
+ "TCL", "TCP-wrappers", "TMate", "TORQUE-1.1", "TOSL", "TU-Berlin-1.0",
+ "TU-Berlin-2.0", "u-boot-exception-2.0", "UCL-1.0", "Unicode-DFS-2015",
+ "Unicode-DFS-2016", "Unicode-TOU", "Universal-FOSS-exception-1.0", "Unlicense",
+ "UPL-1.0", "Vim", "VOSTROM", "VSL-1.0", "W3C-19980720", "W3C-20150513", "W3C",
+ "Watcom-1.0", "Wsuipa", "WTFPL", "WxWindows-exception-3.1", "wxWindows", "X11",
+ "Xerox", "XFree86-1.1", "xinetd", "Xnet", "xpp", "XSkat", "YPL-1.0", "YPL-1.1",
+ "Zed", "Zend-2.0", "Zimbra-1.3", "Zimbra-1.4", "zlib-acknowledgement", "Zlib",
+ "ZPL-1.1", "ZPL-2.0", "ZPL-2.1",
+ }
+}
diff --git a/rdfloader/parser2v3/license_utils.go b/rdfloader/parser2v3/license_utils.go
new file mode 100644
index 0000000..11bb3c8
--- /dev/null
+++ b/rdfloader/parser2v3/license_utils.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+/* util methods for licenses and checksums below:*/
+
+// Given the license URI, returns the name of the license defined
+// in the last part of the uri.
+// This function is susceptible to false-positives.
+func getLicenseStringFromURI(uri string) string {
+ licenseEnd := strings.TrimSpace(getLastPartOfURI(uri))
+ lower := strings.ToLower(licenseEnd)
+ if lower == "none" || lower == "noassertion" {
+ return strings.ToUpper(licenseEnd)
+ }
+ return licenseEnd
+}
+
+// returns the checksum algorithm and it's value
+// In the newer versions, these two strings will be bound to a single checksum struct
+// whose pointer will be returned.
+func (parser *rdfParser2_3) getChecksumFromNode(checksumNode *gordfParser.Node) (algorithm common.ChecksumAlgorithm, value string, err error) {
+ var checksumValue, checksumAlgorithm string
+ for _, checksumTriple := range parser.nodeToTriples(checksumNode) {
+ switch checksumTriple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_CHECKSUM_VALUE:
+ // cardinality: exactly 1
+ checksumValue = strings.TrimSpace(checksumTriple.Object.ID)
+ case SPDX_ALGORITHM:
+ // cardinality: exactly 1
+ checksumAlgorithm, err = getAlgorithmFromURI(checksumTriple.Object.ID)
+ if err != nil {
+ return
+ }
+ default:
+ err = fmt.Errorf("unknown predicate '%s' while parsing checksum node", checksumTriple.Predicate.ID)
+ return
+ }
+ }
+ return common.ChecksumAlgorithm(checksumAlgorithm), checksumValue, nil
+}
+
+func getAlgorithmFromURI(algorithmURI string) (checksumAlgorithm string, err error) {
+ fragment := getLastPartOfURI(algorithmURI)
+ if !strings.HasPrefix(fragment, "checksumAlgorithm_") {
+ return "", fmt.Errorf("checksum algorithm uri must begin with checksumAlgorithm_. found %s", fragment)
+ }
+ algorithm := strings.TrimPrefix(fragment, "checksumAlgorithm_")
+ algorithm = strings.ToLower(strings.TrimSpace(algorithm))
+ switch algorithm {
+ case "md2", "md4", "md5", "md6":
+ checksumAlgorithm = strings.ToUpper(algorithm)
+ case "sha1", "sha224", "sha256", "sha384", "sha512":
+ checksumAlgorithm = strings.ToUpper(algorithm)
+ default:
+ return "", fmt.Errorf("unknown checksum algorithm %s", algorithm)
+ }
+ return
+}
+
+// from a list of licenses, it returns a
+// list of string representation of those licenses.
+func mapLicensesToStrings(licences []AnyLicenseInfo) []string {
+ res := make([]string, len(licences), len(licences))
+ for i, lic := range licences {
+ res[i] = lic.ToLicenseString()
+ }
+ return res
+}
+
+/****** Type Functions ******/
+
+// TODO: should probably add brackets while linearizing a nested license.
+func (lic ConjunctiveLicenseSet) ToLicenseString() string {
+ return strings.Join(mapLicensesToStrings(lic.members), " AND ")
+}
+
+// TODO: should probably add brackets while linearizing a nested license.
+func (lic DisjunctiveLicenseSet) ToLicenseString() string {
+ return strings.Join(mapLicensesToStrings(lic.members), " OR ")
+}
+
+func (lic ExtractedLicensingInfo) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (operator OrLaterOperator) ToLicenseString() string {
+ return operator.member.ToLicenseString()
+}
+
+func (lic License) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (lic ListedLicense) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (lic WithExceptionOperator) ToLicenseString() string {
+ return lic.member.ToLicenseString()
+}
+
+func (lic SpecialLicense) ToLicenseString() string {
+ return string(lic.value)
+}
+
+func (lic SimpleLicensingInfo) ToLicenseString() string {
+ return lic.licenseID
+}
diff --git a/rdfloader/parser2v3/license_utils_test.go b/rdfloader/parser2v3/license_utils_test.go
new file mode 100644
index 0000000..0156b99
--- /dev/null
+++ b/rdfloader/parser2v3/license_utils_test.go
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+)
+
+func Test_getLicenseStringFromURI(t *testing.T) {
+ // TestCase 1: NONE license
+ input := SPDX_NONE_CAPS
+ output := getLicenseStringFromURI(input)
+ expectedOutput := "NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+
+ // TestCase 2: NOASSERTION license
+ input = SPDX_NOASSERTION_SMALL
+ output = getLicenseStringFromURI(input)
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+
+ // TestCase 3: Other license
+ input = NS_SPDX + "LicenseRef-1"
+ output = getLicenseStringFromURI(input)
+ expectedOutput = "LicenseRef-1"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+}
+
+func Test_rdfParser2_3_getChecksumFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ // TestCase 1: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ `)
+ checksumNode := parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getChecksumFromNode(checksumNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid checksum algorithm")
+ }
+
+ // TestCase 2: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ <spdx:invalidPredicate />
+ </spdx:Checksum>
+ `)
+ checksumNode = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getChecksumFromNode(checksumNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate")
+ }
+
+ // TestCase 3: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ `)
+ checksumNode = parser.gordfParserObj.Triples[0].Subject
+ algorithm, value, err := parser.getChecksumFromNode(checksumNode)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if algorithm != "SHA1" {
+ t.Errorf("expected checksum algorithm to be sha1, found %s", algorithm)
+ }
+ expectedValue := "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ if value != expectedValue {
+ t.Errorf("expected checksumValue to be %s, found %s", expectedValue, value)
+ }
+}
+
+func Test_rdfParser2_3_getAlgorithmFromURI(t *testing.T) {
+ var algorithmURI string
+ var err error
+
+ // TestCase 1: checksumAlgorithm uri doesn't start with checksumAlgorithm_
+ algorithmURI = NS_SPDX + "sha1"
+ _, err = getAlgorithmFromURI(algorithmURI)
+ if err == nil {
+ t.Errorf("should've raised an error for algorithmURI that doesn't start with checksumAlgorithm_")
+ }
+
+ // TestCase 2: unknown checksum algorithm
+ algorithmURI = NS_SPDX + "checksumAlgorithm_sha999"
+ _, err = getAlgorithmFromURI(algorithmURI)
+ if err == nil {
+ t.Errorf("should've raised an error for invalid algorithm")
+ }
+
+ // TestCase 3: valid input
+ algorithmURI = NS_SPDX + "checksumAlgorithm_sha256"
+ algorithm, err := getAlgorithmFromURI(algorithmURI)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if algorithm != "SHA256" {
+ t.Errorf("expected: SHA256, found: %s", algorithm)
+ }
+}
+
+func Test_mapLicensesToStrings(t *testing.T) {
+ // nothing much to test here.
+ // just a dummy dry run.
+ licenses := []AnyLicenseInfo{
+ SpecialLicense{
+ value: NONE,
+ },
+ SpecialLicense{
+ value: NOASSERTION,
+ },
+ }
+ licenseStrings := mapLicensesToStrings(licenses)
+ expectedLicenseStrings := []string{"NONE", "NOASSERTION"}
+ if !reflect.DeepEqual(licenseStrings, expectedLicenseStrings) {
+ t.Errorf("expected: %+v\nfound %+v", expectedLicenseStrings, licenseStrings)
+ }
+}
+
+func TestConjunctiveLicenseSet_ToLicenseString(t *testing.T) {
+ var lic ConjunctiveLicenseSet
+ var output, expectedOutput string
+
+ // TestCase 1: no license in the set
+ lic = ConjunctiveLicenseSet{
+ members: nil,
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = ""
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 2: single license in the set
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 3: more than one license in the set.
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ SpecialLicense{value: NONE},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION AND NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 4: nested conjunctive license.
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: "LicenseRef-1"},
+ SpecialLicense{value: NONE},
+ },
+ },
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION AND LicenseRef-1 AND NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+}
+
+func TestDisjunctiveLicenseSet_ToLicenseString(t *testing.T) {
+ var lic DisjunctiveLicenseSet
+ var output, expectedOutput string
+
+ // TestCase 1: no license in the set
+ lic = DisjunctiveLicenseSet{
+ members: nil,
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = ""
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 2: single license in the set
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 3: more than one license in the set.
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ SpecialLicense{value: NONE},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION OR NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 4: nested conjunctive license.
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: "LicenseRef-1"},
+ SpecialLicense{value: NONE},
+ },
+ },
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION OR LicenseRef-1 OR NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+}
+
+func TestExtractedLicensingInfo_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ extractedLicense := ExtractedLicensingInfo{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ extractedText: "extracted Text",
+ }
+ expectedOutput := "license"
+ output := extractedLicense.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestOrLaterOperator_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ orLater := OrLaterOperator{
+ member: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ }
+ expectedOutput := "license"
+ output := orLater.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ license := License{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ }
+ expectedOutput := "license"
+ output := license.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestListedLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ ll := ListedLicense{License{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ },
+ }
+ expectedOutput := "license"
+ output := ll.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestWithExceptionOperator_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ withException := WithExceptionOperator{
+ member: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ licenseException: LicenseException{},
+ }
+ expectedOutput := "license"
+ output := withException.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestSpecialLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ specialLicense := SpecialLicense{
+ value: "license",
+ }
+ expectedOutput := "license"
+ output := specialLicense.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestSimpleLicensingInfo_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ sli := SimpleLicensingInfo{
+ licenseID: "license",
+ }
+ expectedOutput := "license"
+ output := sli.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_annotation.go b/rdfloader/parser2v3/parse_annotation.go
new file mode 100644
index 0000000..507158a
--- /dev/null
+++ b/rdfloader/parser2v3/parse_annotation.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// creates a new instance of annotation and sets the annotation attributes
+// associated with the given node.
+// The newly created annotation is appended to the doc.
+func (parser *rdfParser2_3) parseAnnotationFromNode(node *gordfParser.Node) (err error) {
+ ann := &v2_3.Annotation{}
+ for _, subTriple := range parser.nodeToTriples(node) {
+ switch subTriple.Predicate.ID {
+ case SPDX_ANNOTATOR:
+ // cardinality: exactly 1
+ err = setAnnotatorFromString(subTriple.Object.ID, ann)
+ case SPDX_ANNOTATION_DATE:
+ // cardinality: exactly 1
+ ann.AnnotationDate = subTriple.Object.ID
+ case RDFS_COMMENT:
+ // cardinality: exactly 1
+ ann.AnnotationComment = subTriple.Object.ID
+ case SPDX_ANNOTATION_TYPE:
+ // cardinality: exactly 1
+ err = setAnnotationType(subTriple.Object.ID, ann)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ default:
+ err = fmt.Errorf("unknown predicate %s while parsing annotation", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return setAnnotationToParser(parser, ann)
+}
+
+func setAnnotationToParser(parser *rdfParser2_3, annotation *v2_3.Annotation) error {
+ if parser.doc == nil {
+ return errors.New("uninitialized spdx document")
+ }
+ if parser.doc.Annotations == nil {
+ parser.doc.Annotations = []*v2_3.Annotation{}
+ }
+ parser.doc.Annotations = append(parser.doc.Annotations, annotation)
+ return nil
+}
+
+// annotator is of type [Person|Organization|Tool]:String
+func setAnnotatorFromString(annotatorString string, ann *v2_3.Annotation) error {
+ subkey, subvalue, err := ExtractSubs(annotatorString, ":")
+ if err != nil {
+ return err
+ }
+ if subkey == "Person" || subkey == "Organization" || subkey == "Tool" {
+ ann.Annotator.AnnotatorType = subkey
+ ann.Annotator.Annotator = subvalue
+ return nil
+ }
+ return fmt.Errorf("unrecognized Annotator type %v while parsing annotation", subkey)
+}
+
+// it can be NS_SPDX+annotationType_[review|other]
+func setAnnotationType(annType string, ann *v2_3.Annotation) error {
+ switch annType {
+ case SPDX_ANNOTATION_TYPE_OTHER:
+ ann.AnnotationType = "OTHER"
+ case SPDX_ANNOTATION_TYPE_REVIEW:
+ ann.AnnotationType = "REVIEW"
+ default:
+ return fmt.Errorf("unknown annotation type %s", annType)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_annotation_test.go b/rdfloader/parser2v3/parse_annotation_test.go
new file mode 100644
index 0000000..1ce0bd7
--- /dev/null
+++ b/rdfloader/parser2v3/parse_annotation_test.go
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_setAnnotatorFromString(t *testing.T) {
+ // TestCase 1: Empty String must raise an error
+ ann := &v2_3.Annotation{}
+ input := ""
+ err := setAnnotatorFromString(input, ann)
+ if err == nil {
+ t.Error("should've raised an error for an empty string")
+ }
+
+ // TestCase 2: Invalid annotator type
+ ann = &v2_3.Annotation{}
+ input = "Company: some_company"
+ err = setAnnotatorFromString(input, ann)
+ if err == nil {
+ t.Errorf("should've raised an error for an unknown annotator type")
+ }
+
+ // TestCase 3: Valid annotator
+ ann = &v2_3.Annotation{}
+ input = "Person: Rishabh"
+ err = setAnnotatorFromString(input, ann)
+ if err != nil {
+ t.Errorf("unexpected error for a valid annotator")
+ }
+ if ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf("wrnog annotator type: expected: %s, found: %s", "Person", ann.Annotator)
+ }
+ if ann.Annotator.Annotator != "Rishabh" {
+ t.Errorf("wrong annotator: expected: %s, found: %s", "Rishabh", ann.Annotator)
+ }
+}
+
+func Test_setAnnotationType(t *testing.T) {
+ ann := &v2_3.Annotation{}
+ // TestCase 1: invalid input (empty annotationType)
+ err := setAnnotationType("", ann)
+ if err == nil {
+ t.Errorf("expected an error for empty input")
+ }
+
+ // TestCase 2: invalid input (unknown annotation type)
+ err = setAnnotationType(NS_SPDX+"annotationType_unknown", ann)
+ if err == nil {
+ t.Errorf("expected an error for invalid annotationType")
+ }
+
+ // TestCase 3: valid input (annotationType_other)
+ err = setAnnotationType(SPDX_ANNOTATION_TYPE_OTHER, ann)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ann.AnnotationType != "OTHER" {
+ t.Errorf("expected: OTHER, found: %s", ann.AnnotationType)
+ }
+
+ // TestCase 4: valid input (annotationType_review)
+ err = setAnnotationType(SPDX_ANNOTATION_TYPE_REVIEW, ann)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ann.AnnotationType != "REVIEW" {
+ t.Errorf("expected: REVIEW, found: %s", ann.AnnotationType)
+ }
+}
+
+func Test_setAnnotationToParser(t *testing.T) {
+ // TestCase 1: doc is nil (must raise an error)
+ parser, _ := parserFromBodyContent(``)
+ parser.doc = nil
+ err := setAnnotationToParser(parser, &v2_3.Annotation{})
+ if err == nil {
+ t.Errorf("empty doc should've raised an error")
+ }
+
+ // TestCase 2: empty annotations should create a new annotations
+ // list and append the input to it.
+ parser, _ = parserFromBodyContent(``)
+ parser.doc.Annotations = nil
+ err = setAnnotationToParser(parser, &v2_3.Annotation{})
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if len(parser.doc.Annotations) != 1 {
+ t.Errorf("expected doc to have 1 annotation, found %d", len(parser.doc.Annotations))
+ }
+}
+
+func Test_rdfParser2_3_parseAnnotationFromNode(t *testing.T) {
+ // TestCase 1: invalid annotator must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Company: some company</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ err := parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("wrong annotator type should've raised an error")
+ }
+
+ // TestCase 2: wrong annotation type should raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_unknown"/>
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("wrong annotation type should've raised an error")
+ }
+
+ // TestCase 3: unknown predicate should also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_unknown"/>
+ <spdx:unknownPredicate />
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("unknown predicate must raise an error")
+ }
+
+ // TestCase 4: completely valid annotation
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing valid a annotation")
+ }
+ if n := len(parser.doc.Annotations); n != 1 {
+ t.Errorf("document should've had only one annotation, found %d", n)
+ }
+ ann := parser.doc.Annotations[0]
+ // validating all the attributes of the annotations
+ expectedComment := "Document level annotation"
+ if ann.AnnotationComment != expectedComment {
+ t.Errorf(`expected: "%s", found "%s"`, expectedComment, ann.AnnotationComment)
+ }
+ expectedDate := "2010-01-29T18:30:22Z"
+ if expectedDate != ann.AnnotationDate {
+ t.Errorf(`expected: "%s", found "%s"`, expectedDate, ann.AnnotationDate)
+ }
+ expectedAnnotator := "Jane Doe"
+ if expectedAnnotator != ann.Annotator.Annotator {
+ t.Errorf(`expected: "%s", found "%s"`, expectedAnnotator, ann.Annotator)
+ }
+ if ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf(`expected: "%s", found "%s"`, "Person", ann.Annotator.AnnotatorType)
+ }
+ expectedAnnotationType := "OTHER"
+ if expectedAnnotationType != ann.AnnotationType {
+ t.Errorf(`expected: "%s", found "%s"`, expectedAnnotationType, ann.AnnotationType)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_creation_info.go b/rdfloader/parser2v3/parse_creation_info.go
new file mode 100644
index 0000000..93a5628
--- /dev/null
+++ b/rdfloader/parser2v3/parse_creation_info.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// Cardinality: Mandatory, one.
+func (parser *rdfParser2_3) parseCreationInfoFromNode(ci *v2_3.CreationInfo, node *gordfParser.Node) error {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_LICENSE_LIST_VERSION: // 2.7
+ // cardinality: max 1
+ ci.LicenseListVersion = triple.Object.ID
+ case SPDX_CREATOR: // 2.8
+ // cardinality: min 1
+ err := setCreator(triple.Object.ID, ci)
+ if err != nil {
+ return err
+ }
+ case SPDX_CREATED: // 2.9
+ // cardinality: exactly 1
+ ci.Created = triple.Object.ID
+ case RDFS_COMMENT: // 2.10
+ ci.CreatorComment = triple.Object.ID
+ case RDF_TYPE:
+ continue
+ default:
+ return fmt.Errorf("unknown predicate %v while parsing a creation info", triple.Predicate)
+ }
+ }
+ return nil
+}
+
+func setCreator(creatorStr string, ci *v2_3.CreationInfo) error {
+ entityType, entity, err := ExtractSubs(creatorStr, ":")
+ if err != nil {
+ return fmt.Errorf("error setting creator of a creation info: %s", err)
+ }
+
+ creator := common.Creator{Creator: entity}
+
+ switch entityType {
+ case "Person", "Organization", "Tool":
+ creator.CreatorType = entityType
+ default:
+ return fmt.Errorf("unknown creatorType %v in a creation info", entityType)
+ }
+
+ ci.Creators = append(ci.Creators, creator)
+
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_creation_info_test.go b/rdfloader/parser2v3/parse_creation_info_test.go
new file mode 100644
index 0000000..354ac73
--- /dev/null
+++ b/rdfloader/parser2v3/parse_creation_info_test.go
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_setCreator(t *testing.T) {
+ // TestCase 1: invalid creator (empty)
+ input := ""
+ err := setCreator(input, &v2_3.CreationInfo{})
+ if err == nil {
+ t.Errorf("shoud've raised an error due to invalid input")
+ }
+
+ // TestCase 2: invalid entity type
+ input = "Company: some company"
+ err = setCreator(input, &v2_3.CreationInfo{})
+ if err == nil {
+ t.Errorf("shoud've raised an error due to unknown entity type")
+ }
+
+ // TestCase 3: valid input
+ input = "Person: Jane Doe"
+ ci := &v2_3.CreationInfo{}
+ err = setCreator(input, ci)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ if len(ci.Creators) != 1 {
+ t.Errorf("creationInfo should've had 1 creatorPersons, found %d", len(ci.Creators))
+ }
+ expectedPerson := "Jane Doe"
+ if ci.Creators[0].Creator != expectedPerson {
+ t.Errorf("expected %s, found %s", expectedPerson, ci.Creators[0])
+ }
+}
+
+func Test_rdfParser2_3_parseCreationInfoFromNode(t *testing.T) {
+ // TestCase 1: invalid creator must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person Unknown</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ </spdx:CreationInfo>
+ `)
+ ciNode := parser.gordfParserObj.Triples[0].Subject
+ err := parser.parseCreationInfoFromNode(&v2_3.CreationInfo{}, ciNode)
+ if err == nil {
+ t.Errorf("invalid creator must raise an error")
+ }
+
+ // TestCase 2: unknown predicate must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: fossy (y)</spdx:creator>
+ <spdx:creator>Organization: </spdx:creator>
+ <spdx:creator>Tool: spdx2</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ <spdx:unknownPredicate />
+ </spdx:CreationInfo>
+ `)
+ ciNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseCreationInfoFromNode(&v2_3.CreationInfo{}, ciNode)
+ if err == nil {
+ t.Errorf("unknown predicate must raise an error")
+ }
+
+ // TestCase 2: unknown predicate must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: fossy</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:CreationInfo>
+ `)
+ ciNode = parser.gordfParserObj.Triples[0].Subject
+ ci := &v2_3.CreationInfo{}
+ err = parser.parseCreationInfoFromNode(ci, ciNode)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ci.LicenseListVersion != "2.6" {
+ t.Errorf(`expected %s, found %s`, "2.6", ci.LicenseListVersion)
+ }
+ n := len(ci.Creators)
+ if n != 1 {
+ t.Errorf("expected 1 creatorPersons, found %d", n)
+ }
+ if ci.Creators[0].Creator != "fossy" {
+ t.Errorf("expected %s, found %s", "fossy", ci.Creators[0].Creator)
+ }
+ expectedCreated := "2018-08-24T19:55:34Z"
+ if ci.Created != expectedCreated {
+ t.Errorf("expected %s, found %s", expectedCreated, ci.Created)
+ }
+ expectedComment := "comment"
+ if ci.CreatorComment != expectedComment {
+ t.Errorf("expected %s, found %s", expectedComment, ci.CreatorComment)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_file.go b/rdfloader/parser2v3/parse_file.go
new file mode 100644
index 0000000..a0bc6fd
--- /dev/null
+++ b/rdfloader/parser2v3/parse_file.go
@@ -0,0 +1,207 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// returns a file instance and the error if any encountered.
+func (parser *rdfParser2_3) getFileFromNode(fileNode *gordfParser.Node) (file *v2_3.File, err error) {
+ file = &v2_3.File{}
+
+ currState := parser.cache[fileNode.ID]
+ if currState == nil {
+ // this is the first time we are seeing this node.
+ parser.cache[fileNode.ID] = &nodeState{
+ object: file,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this file node and we needn't parse it again.
+ return currState.object.(*v2_3.File), nil
+ }
+
+ // setting color to grey to indicate that we've started parsing this node.
+ parser.cache[fileNode.ID].Color = GREY
+
+ // setting color to black just before function returns to the caller to
+ // indicate that parsing current node is complete.
+ defer func() { parser.cache[fileNode.ID].Color = BLACK }()
+
+ err = setFileIdentifier(fileNode.ID, file) // 4.2
+ if err != nil {
+ return nil, err
+ }
+
+ if existingFile := parser.files[file.FileSPDXIdentifier]; existingFile != nil {
+ file = existingFile
+ }
+
+ for _, subTriple := range parser.nodeToTriples(fileNode) {
+ switch subTriple.Predicate.ID {
+ case SPDX_FILE_NAME: // 4.1
+ // cardinality: exactly 1
+ file.FileName = subTriple.Object.ID
+ case SPDX_NAME:
+ // cardinality: exactly 1
+ // TODO: check where it will be set in the golang-tools spdx-data-model
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ case SPDX_FILE_TYPE: // 4.3
+ // cardinality: min 0
+ fileType := ""
+ fileType, err = parser.getFileTypeFromUri(subTriple.Object.ID)
+ file.FileTypes = append(file.FileTypes, fileType)
+ case SPDX_CHECKSUM: // 4.4
+ // cardinality: min 1
+ err = parser.setFileChecksumFromNode(file, subTriple.Object)
+ case SPDX_LICENSE_CONCLUDED: // 4.5
+ // cardinality: (exactly 1 anyLicenseInfo) or (None) or (Noassertion)
+ anyLicense, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing licenseConcluded: %v", err)
+ }
+ file.LicenseConcluded = anyLicense.ToLicenseString()
+ case SPDX_LICENSE_INFO_IN_FILE: // 4.6
+ // cardinality: min 1
+ lic, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing licenseInfoInFile: %v", err)
+ }
+ file.LicenseInfoInFiles = append(file.LicenseInfoInFiles, lic.ToLicenseString())
+ case SPDX_LICENSE_COMMENTS: // 4.7
+ // cardinality: max 1
+ file.LicenseComments = subTriple.Object.ID
+ // TODO: allow copyright text to be of type NOASSERTION
+ case SPDX_COPYRIGHT_TEXT: // 4.8
+ // cardinality: exactly 1
+ file.FileCopyrightText = subTriple.Object.ID
+ case SPDX_LICENSE_INFO_FROM_FILES:
+ // TODO: implement it. It is not defined in the tools-golang model.
+ // deprecated artifactOf (see sections 4.9, 4.10, 4.11)
+ case SPDX_ARTIFACT_OF:
+ // cardinality: min 0
+ var artifactOf *v2_3.ArtifactOfProject
+ artifactOf, err = parser.getArtifactFromNode(subTriple.Object)
+ file.ArtifactOfProjects = append(file.ArtifactOfProjects, artifactOf)
+ case RDFS_COMMENT: // 4.12
+ // cardinality: max 1
+ file.FileComment = subTriple.Object.ID
+ case SPDX_NOTICE_TEXT: // 4.13
+ // cardinality: max 1
+ file.FileNotice = getNoticeTextFromNode(subTriple.Object)
+ case SPDX_FILE_CONTRIBUTOR: // 4.14
+ // cardinality: min 0
+ file.FileContributors = append(file.FileContributors, subTriple.Object.ID)
+ case SPDX_FILE_DEPENDENCY:
+ // cardinality: min 0
+ newFile, err := parser.getFileFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error setting a file dependency in a file: %v", err)
+ }
+ file.FileDependencies = append(file.FileDependencies, string(newFile.FileSPDXIdentifier))
+ case SPDX_ATTRIBUTION_TEXT:
+ // cardinality: min 0
+ file.FileAttributionTexts = append(file.FileAttributionTexts, subTriple.Object.ID)
+ case SPDX_ANNOTATION:
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ case SPDX_RELATIONSHIP:
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ default:
+ return nil, fmt.Errorf("unknown triple predicate id %s", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+ parser.files[file.FileSPDXIdentifier] = file
+ return file, nil
+}
+
+func (parser *rdfParser2_3) setFileChecksumFromNode(file *v2_3.File, checksumNode *gordfParser.Node) error {
+ checksumAlgorithm, checksumValue, err := parser.getChecksumFromNode(checksumNode)
+ if err != nil {
+ return fmt.Errorf("error parsing checksumNode of a file: %v", err)
+ }
+ if file.Checksums == nil {
+ file.Checksums = []common.Checksum{}
+ }
+ switch checksumAlgorithm {
+ case common.MD5, common.SHA1, common.SHA256:
+ file.Checksums = append(file.Checksums, common.Checksum{Algorithm: checksumAlgorithm, Value: checksumValue})
+ case "":
+ return fmt.Errorf("empty checksum algorithm and value")
+ default:
+ return fmt.Errorf("unknown checksumAlgorithm %s for a file", checksumAlgorithm)
+ }
+ return nil
+}
+
+func (parser *rdfParser2_3) getArtifactFromNode(node *gordfParser.Node) (*v2_3.ArtifactOfProject, error) {
+ artifactOf := &v2_3.ArtifactOfProject{}
+ // setting artifactOfProjectURI attribute (which is optional)
+ if node.NodeType == gordfParser.IRI {
+ artifactOf.URI = node.ID
+ }
+ // parsing rest triples and attributes of the artifact.
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ case DOAP_HOMEPAGE:
+ artifactOf.HomePage = triple.Object.ID
+ case DOAP_NAME:
+ artifactOf.Name = triple.Object.ID
+ default:
+ return nil, fmt.Errorf("error parsing artifactOf predicate %s", triple.Predicate.ID)
+ }
+ }
+ return artifactOf, nil
+}
+
+// TODO: check if the filetype is valid.
+func (parser *rdfParser2_3) getFileTypeFromUri(uri string) (string, error) {
+ // fileType is given as a uri. for example: http://spdx.org/rdf/terms#fileType_text
+ lastPart := getLastPartOfURI(uri)
+ if !strings.HasPrefix(lastPart, "fileType_") {
+ return "", fmt.Errorf("fileType Uri must begin with fileTYpe_. found: %s", lastPart)
+ }
+ return strings.TrimPrefix(lastPart, "fileType_"), nil
+}
+
+// populates parser.doc.Files by a list of files which are not
+// associated with a package by the hasFile attribute
+// assumes: all the packages are already parsed.
+func (parser *rdfParser2_3) setUnpackagedFiles() {
+ for fileID := range parser.files {
+ if !parser.assocWithPackage[fileID] {
+ parser.doc.Files = append(parser.doc.Files, parser.files[fileID])
+ }
+ }
+}
+
+func setFileIdentifier(idURI string, file *v2_3.File) (err error) {
+ idURI = strings.TrimSpace(idURI)
+ uriFragment := getLastPartOfURI(idURI)
+ file.FileSPDXIdentifier, err = ExtractElementID(uriFragment)
+ if err != nil {
+ return fmt.Errorf("error setting file identifier: %s", err)
+ }
+ return nil
+}
+
+func getNoticeTextFromNode(node *gordfParser.Node) string {
+ switch node.ID {
+ case SPDX_NOASSERTION_CAPS, SPDX_NOASSERTION_SMALL:
+ return "NOASSERTION"
+ default:
+ return node.ID
+ }
+}
diff --git a/rdfloader/parser2v3/parse_file_test.go b/rdfloader/parser2v3/parse_file_test.go
new file mode 100644
index 0000000..3ea79bb
--- /dev/null
+++ b/rdfloader/parser2v3/parse_file_test.go
@@ -0,0 +1,765 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "bufio"
+ "strings"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ rdfloader2 "github.com/spdx/gordf/rdfloader/xmlreader"
+ gordfWriter "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// content is the tags within the rdf:RDF tag
+// pads the content with the enclosing rdf:RDF tag
+func wrapIntoTemplate(content string) string {
+ header := `<rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#"
+ xmlns:doap="http://usefulinc.com/ns/doap#"
+ xmlns:j.0="http://www.w3.org/2009/pointers#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">`
+ footer := `</rdf:RDF>`
+ return header + content + footer
+}
+
+func parserFromBodyContent(content string) (*rdfParser2_3, error) {
+ rdfContent := wrapIntoTemplate(content)
+ xmlreader := rdfloader2.XMLReaderFromFileObject(bufio.NewReader(strings.NewReader(rdfContent)))
+ rootBlock, err := xmlreader.Read()
+ if err != nil {
+ return nil, err
+ }
+ parser := gordfParser.New()
+ err = parser.Parse(rootBlock)
+ if err != nil {
+ return nil, err
+ }
+ nodeToTriples := gordfWriter.GetNodeToTriples(parser.Triples)
+ rdfParser := NewParser2_3(parser, nodeToTriples)
+ return rdfParser, err
+}
+
+func Test_rdfParser2_3_getArtifactFromNode(t *testing.T) {
+ // TestCase 1: artifactOf without project URI
+ rdfParser, err := parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode := gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ artifact, err := rdfParser.getArtifactFromNode(artifactOfNode)
+ if err != nil {
+ t.Errorf("error parsing a valid artifactOf node: %v", err)
+ }
+ if artifact.Name != "Jena" {
+ t.Errorf("expected name of artifact: %s, found: %s", "Jena", artifact.Name)
+ }
+ expectedHomePage := "http://www.openjena.org/"
+ if artifact.HomePage != expectedHomePage {
+ t.Errorf("wrong artifact homepage. Expected: %s, found: %s", expectedHomePage, artifact.HomePage)
+ }
+ if artifact.URI != "" {
+ t.Errorf("wrong artifact URI. Expected: %s, found: %s", "", artifact.URI)
+ }
+
+ // TestCase 2: artifactOf with a Project URI
+ rdfParser, err = parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode = gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ artifact, err = rdfParser.getArtifactFromNode(artifactOfNode)
+ if err != nil {
+ t.Errorf("error parsing a valid artifactOf node: %v", err)
+ }
+ expectedURI := "http://subversion.apache.org/doap.rdf"
+ if artifact.URI != expectedURI {
+ t.Errorf("wrong artifact URI. Expected: %s, found: %s", expectedURI, artifact.URI)
+ }
+
+ // TestCase 3: artifactOf with unknown predicate
+ rdfParser, err = parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ <doap:invalidTag rdf:ID="invalid"/>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode = gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ _, err = rdfParser.getArtifactFromNode(artifactOfNode)
+ if err == nil {
+ t.Errorf("must've raised an error for an invalid predicate")
+ }
+}
+
+func Test_rdfParser2_3_getFileTypeFromUri(t *testing.T) {
+ rdfParser, _ := parserFromBodyContent(``)
+
+ // TestCase 1: Valid fileType URI:
+ fileTypeURI := "http://spdx.org/rdf/terms#fileType_source"
+ fileType, err := rdfParser.getFileTypeFromUri(fileTypeURI)
+ if err != nil {
+ t.Errorf("error in a valid example: %v", err)
+ }
+ if fileType != "source" {
+ t.Errorf("wrong fileType. expected: %s, found: %s", "source", fileType)
+ }
+
+ // TestCase 2: Invalid fileType URI format.
+ fileTypeURI = "http://spdx.org/rdf/terms#source"
+ fileType, err = rdfParser.getFileTypeFromUri(fileTypeURI)
+ if err == nil {
+ t.Error("should've raised an error for invalid fileType")
+ }
+}
+
+func Test_rdfParser2_3_setUnpackagedFiles(t *testing.T) {
+ // unpackaged files are the files which are not associated with any package
+ // file associated with a package sets parser.assocWithPackage[fileID] to true.
+ rdfParser, _ := parserFromBodyContent(``)
+ file1 := &v2_3.File{FileSPDXIdentifier: common.ElementID("file1")}
+ file2 := &v2_3.File{FileSPDXIdentifier: common.ElementID("file2")}
+ file3 := &v2_3.File{FileSPDXIdentifier: common.ElementID("file3")}
+
+ // setting files to the document as if it were to be set when it was parsed using triples.
+ rdfParser.files[file1.FileSPDXIdentifier] = file1
+ rdfParser.files[file2.FileSPDXIdentifier] = file2
+ rdfParser.files[file3.FileSPDXIdentifier] = file3
+
+ // assuming file1 is associated with a package
+ rdfParser.assocWithPackage[file1.FileSPDXIdentifier] = true
+
+ rdfParser.setUnpackagedFiles()
+
+ // after setting unpackaged files, parser.doc.Files must've file2 and file3
+ if n := len(rdfParser.doc.Files); n != 2 {
+ t.Errorf("unpackage files should've had 2 files, found %d files", n)
+ }
+
+ // checking if the unpackagedFiles contain only file2 & file3.
+ for _, file := range rdfParser.doc.Files {
+ switch string(file.FileSPDXIdentifier) {
+ case "file2", "file3":
+ continue
+ default:
+ t.Errorf("unexpected file with id %s found in unpackaged files", file.FileSPDXIdentifier)
+ }
+ }
+}
+
+func Test_setFileIdentifier(t *testing.T) {
+ file := &v2_3.File{}
+
+ // TestCase 1: valid example
+ err := setFileIdentifier("http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-129", file)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if file.FileSPDXIdentifier != "129" {
+ t.Errorf("expected %s, found: %s", "129", file.FileSPDXIdentifier)
+ }
+
+ // TestCase 2: invalid example
+ err = setFileIdentifier("http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#129", file)
+ if err == nil {
+ t.Errorf("should've raised an error for an invalid example")
+ }
+}
+
+func Test_rdfParser2_3_setFileChecksumFromNode(t *testing.T) {
+ // TestCase 1: md5 checksum
+ parser, _ := parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file := &v2_3.File{}
+ err := parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ checksumValue := "d2356e0fe1c0b85285d83c6b2ad51b5f"
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha1, should've been empty")
+ }
+ case common.SHA256:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha256, should've been empty")
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for md5. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 2: valid sha1 checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for sha1. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha256, should've been empty")
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set md5, should've been empty")
+ }
+ }
+ }
+
+ // TestCase 3: valid sha256 checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set sha1, should've been empty")
+ }
+ case common.SHA256:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for sha256. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set md5, should've been empty")
+ }
+ }
+ }
+
+ // TestCase 4: checksum node without one of the mandatory attributes
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum node")
+ }
+
+ // TestCase 5: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md43" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum node")
+ }
+
+ // TestCase 6: valid checksum algorithm which is invalid for file (like md4, md6, sha384, etc.)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha384" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum algorithm for a file")
+ }
+}
+
+func Test_rdfParser2_3_getFileFromNode(t *testing.T) {
+ // TestCase 1: file with invalid id
+ parser, _ := parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gzspdx.rdf#item177"/>
+ `)
+ fileNode := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err := parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid file ID")
+ }
+
+ // TestCase 2: invalid fileType
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#source"/>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid fileType")
+ }
+
+ // TestCase 3: invalid file checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha384" />
+ <spdx:checksumValue>0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid checksum")
+ }
+
+ // TestCase 4: invalid license concluded
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#invalid_license" />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid license Concluded")
+ }
+
+ // TestCase 5: invalid artifactOf attribute
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:unknown_tag />
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid artifactOf predicate")
+ }
+
+ // TestCase 6: invalid file dependency
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example#CommonsLangSrc"/>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid fileDependency")
+ }
+
+ // TestCase 7: invalid annotation with unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid annotation predicate")
+ }
+
+ // TestCase 8: invalid relationship
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#dynamicLink"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid relationship Type")
+ }
+
+ // TestCase 8: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:unknown />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Error("should've raised an error stating invalid predicate for a file")
+ }
+
+ // TestCase 9: invalid licenseInfoInFile.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/DC0-1.0" />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Error("should've raised an error stating invalid licenseInfoInFile for a file")
+ }
+
+ // TestCase 10: Splitting of File definition into parents of different tags mustn't create new file objects.
+ fileDefinitions := []string{
+ `<spdx:Package rdf:about="#SPDXRef-Package1">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileName>time-1.9/ChangeLog</spdx:fileName>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ </spdx:File>
+ </spdx:hasFile>
+ </spdx:Package>`,
+ `<spdx:Package rdf:about="#SPDXRef-Package2">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/rdf/terms#NOASSERTION" />
+ </spdx:File>
+ </spdx:hasFile>
+ </spdx:Package>`,
+ }
+ parser, _ = parserFromBodyContent(strings.Join(fileDefinitions, ""))
+
+ var file *v2_3.File
+ packageTypeTriples := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)
+ for _, typeTriple := range packageTypeTriples {
+ pkg, err := parser.getPackageFromNode(typeTriple.Subject)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid package: %v", err)
+ }
+ if n := len(pkg.Files); n != 1 {
+ t.Errorf("expected package to contain exactly 1 file. Found %d files", n)
+ }
+ for _, file = range pkg.Files {
+ }
+ }
+
+ // checking if all the attributes that spanned over a several tags are set in the same variable.
+ expectedFileName := "time-1.9/ChangeLog"
+ if file.FileName != expectedFileName {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileName)
+ }
+ expectedLicenseConcluded := "NOASSERTION"
+ if file.LicenseConcluded != expectedLicenseConcluded {
+ t.Errorf("expected %s, found %s", expectedLicenseConcluded, file.LicenseConcluded)
+ }
+ expectedFileType := "source"
+ if file.FileTypes[0] != expectedFileType {
+ t.Errorf("expected %s, found %s", expectedFileType, file.FileTypes)
+ }
+ expectedLicenseInfoInFile := "NOASSERTION"
+ if file.LicenseInfoInFiles[0] != expectedLicenseInfoInFile {
+ t.Errorf("expected %s, found %s", expectedLicenseInfoInFile, file.LicenseInfoInFiles[0])
+ }
+
+ // TestCase 12: checking if recursive dependencies are resolved.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="#SPDXRef-ParentFile">
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:fileDependency>
+ <spdx:File rdf:about="#SPDXRef-ChildFile">
+ <spdx:fileDependency>
+ <spdx:File rdf:about="#SPDXRef-ParentFile">
+ <spdx:fileName>ParentFile</spdx:fileName>
+ </spdx:File>
+ </spdx:fileDependency>
+ </spdx:File>
+ </spdx:fileDependency>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ file, err = parser.getFileFromNode(fileNode)
+
+ // TestCase 11: all valid attribute and it's values.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileName>time-1.9/ChangeLog</spdx:fileName>
+ <spdx:name/>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/rdf/terms#NOASSERTION" />
+ <spdx:licenseComments>no comments</spdx:licenseComments>
+ <spdx:copyrightText>from spdx file</spdx:copyrightText>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:noticeText rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ <spdx:fileContributor>Some Organization</spdx:fileContributor>
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-CommonsLangSrc"/>
+ <spdx:attributionText>attribution text</spdx:attributionText>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>File level annotation copied from a spdx document</rdfs:comment>
+ <spdx:annotator>Person: File Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_contains"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Package"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ file, err = parser.getFileFromNode(fileNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid file: %v", err)
+ }
+
+ // checking each and every attribute of the obtained file.
+
+ expectedFileName = "time-1.9/ChangeLog"
+ if file.FileName != expectedFileName {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileName)
+ }
+
+ if len(file.FileTypes) != 1 {
+ t.Errorf("given file should have 1 fileType attribute. found %d", len(file.FileTypes))
+ }
+ expectedFileType = "source"
+ if file.FileTypes[0] != expectedFileType {
+ t.Errorf("expected %s, found %s", expectedFileType, file.FileTypes)
+ }
+
+ expectedChecksum := "0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd"
+
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != expectedChecksum {
+ t.Errorf("expected %s, found %s", expectedChecksum, checksum.Value)
+ }
+ }
+ }
+
+ expectedLicenseConcluded = "NOASSERTION"
+ if file.LicenseConcluded != expectedLicenseConcluded {
+ t.Errorf("expected %s, found %s", expectedLicenseConcluded, file.LicenseConcluded)
+ }
+
+ if len(file.LicenseInfoInFiles) != 1 {
+ t.Errorf("given file should have 1 licenseInfoInFile attribute. found %d", len(file.LicenseInfoInFiles))
+ }
+ expectedLicenseInfoInFile = "NOASSERTION"
+ if file.LicenseInfoInFiles[0] != expectedLicenseInfoInFile {
+ t.Errorf("expected %s, found %s", expectedLicenseInfoInFile, file.LicenseInfoInFiles[0])
+ }
+
+ expectedLicenseComments := "no comments"
+ if file.LicenseComments != expectedLicenseComments {
+ t.Errorf("expected %s, found %s", expectedLicenseComments, file.LicenseComments)
+ }
+
+ expectedCopyrightText := "from spdx file"
+ if file.FileCopyrightText != expectedCopyrightText {
+ t.Errorf("expected %s, found %s", expectedCopyrightText, file.FileCopyrightText)
+ }
+
+ if n := len(file.ArtifactOfProjects); n != 1 {
+ t.Errorf("given file should have 1 artifactOfProjects attribute. found %d", n)
+ }
+ artifactOf := file.ArtifactOfProjects[0]
+ expectedHomePage := "http://www.openjena.org/"
+ if artifactOf.HomePage != expectedHomePage {
+ t.Errorf("expected %s, found %s", expectedHomePage, artifactOf.HomePage)
+ }
+ if artifactOf.Name != "Jena" {
+ t.Errorf("expected %s, found %s", "Jena", artifactOf.Name)
+ }
+ if artifactOf.URI != "" {
+ t.Errorf("expected artifactOf uri to be empty, found %s", artifactOf.URI)
+ }
+
+ expectedFileComment := "no comments"
+ if file.FileComment != expectedFileComment {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileComment)
+ }
+
+ expectedNoticeText := "NOASSERTION"
+ if file.FileNotice != expectedNoticeText {
+ t.Errorf("expected %s, found %s", expectedNoticeText, file.FileNotice)
+ }
+
+ if n := len(file.FileContributors); n != 1 {
+ t.Errorf("given file should have 1 fileContributor. found %d", n)
+ }
+ expectedFileContributor := "Some Organization"
+ if file.FileContributors[0] != expectedFileContributor {
+ t.Errorf("expected %s, found %s", expectedFileContributor, file.FileContributors)
+ }
+
+ if n := len(file.FileDependencies); n != 1 {
+ t.Errorf("given file should have 1 fileDependencies. found %d", n)
+ }
+ expectedFileDependency := "CommonsLangSrc"
+ if file.FileDependencies[0] != expectedFileDependency {
+ t.Errorf("expected %s, found %s", expectedFileDependency, file.FileDependencies[0])
+ }
+
+ if n := len(file.FileAttributionTexts); n != 1 {
+ t.Errorf("given file should have 1 attributionText. found %d", n)
+ }
+ expectedAttributionText := "attribution text"
+ if file.FileAttributionTexts[0] != expectedAttributionText {
+ t.Errorf("expected %s, found %s", expectedAttributionText, file.FileAttributionTexts[0])
+ }
+
+ if n := len(parser.doc.Annotations); n != 1 {
+ t.Errorf("doc should've had 1 annotation. found %d", n)
+ }
+ ann := parser.doc.Annotations[0]
+ expectedAnnDate := "2011-01-29T18:30:22Z"
+ if ann.AnnotationDate != expectedAnnDate {
+ t.Errorf("expected %s, found %s", expectedAnnDate, ann.AnnotationDate)
+ }
+ expectedAnnComment := "File level annotation copied from a spdx document"
+ if ann.AnnotationComment != expectedAnnComment {
+ t.Errorf("expected %s, found %s", expectedAnnComment, ann.AnnotationComment)
+ }
+ expectedAnnotationType := "OTHER"
+ if ann.AnnotationType != expectedAnnotationType {
+ t.Errorf("expected %s, found %s", expectedAnnotationType, ann.AnnotationType)
+ }
+ expectedAnnotator := "File Commenter"
+ if ann.Annotator.Annotator != expectedAnnotator {
+ t.Errorf("expected %s, found %s", expectedAnnotator, ann.Annotator)
+ }
+ expectedAnnotatorType := "Person"
+ if ann.AnnotationType != expectedAnnotationType {
+ t.Errorf("expected %s, found %s", expectedAnnotatorType, ann.Annotator.AnnotatorType)
+ }
+
+ if n := len(parser.doc.Relationships); n != 1 {
+ t.Errorf("doc should've had 1 relation. found %d", n)
+ }
+ reln := parser.doc.Relationships[0]
+ expectedRefAEID := "item177"
+ if reln.RefA.DocumentRefID != "" {
+ t.Errorf("expected refA.DocumentRefID to be empty, found %s", reln.RefA.DocumentRefID)
+ }
+ if string(reln.RefA.ElementRefID) != expectedRefAEID {
+ t.Errorf("expected %s, found %s", expectedRefAEID, reln.RefA.ElementRefID)
+ }
+ expectedRefBEID := "Package"
+ if reln.RefB.DocumentRefID != "" {
+ t.Errorf("expected refB.DocumentRefID to be empty, found %s", reln.RefB.DocumentRefID)
+ }
+ if string(reln.RefB.ElementRefID) != expectedRefBEID {
+ t.Errorf("expected %s, found %s", expectedRefBEID, reln.RefB.ElementRefID)
+ }
+ expectedRelationType := "contains"
+ if reln.Relationship != expectedRelationType {
+ t.Errorf("expected %s, found %s", expectedRefBEID, reln.RefB.ElementRefID)
+ }
+ if reln.RelationshipComment != "" {
+ t.Errorf("expected relationship comment to be empty, found %s", reln.RelationshipComment)
+ }
+}
+
+func Test_getNoticeTextFromNode(t *testing.T) {
+ // TestCase 1: SPDX_NOASSERTION_SMALL must return NOASSERTION
+ output := getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_NOASSERTION_SMALL,
+ })
+ if strings.ToUpper(output) != "NOASSERTION" {
+ t.Errorf("expected NOASSERTION, found %s", strings.ToUpper(output))
+ }
+
+ // TestCase 2: SPDX_NOASSERTION_CAPS must return NOASSERTION
+ output = getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_NOASSERTION_CAPS,
+ })
+ if strings.ToUpper(output) != "NOASSERTION" {
+ t.Errorf("expected NOASSERTION, found %s", strings.ToUpper(output))
+ }
+
+ // TestCase 3: not a NOASSERTION must return the field verbatim
+ // TestCase 1: SPDX_NOASSERTION_SMALL must return NOASSERTION
+ output = getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "text",
+ })
+ if output != "text" {
+ t.Errorf("expected text, found %s", output)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_license.go b/rdfloader/parser2v3/parse_license.go
new file mode 100644
index 0000000..b75ca05
--- /dev/null
+++ b/rdfloader/parser2v3/parse_license.go
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+)
+
+// AnyLicense is a baseClass for all the licenses
+// All the types of licenses is a sub-type of AnyLicense,
+// either directly or indirectly.
+// This function acts as a mux for all the licenses. Based on the input, it
+// decides which type of license it is and passes control to that type of
+// license parser to parse the given input.
+func (parser *rdfParser2_3) getAnyLicenseFromNode(node *gordfParser.Node) (AnyLicenseInfo, error) {
+
+ currState := parser.cache[node.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[node.ID] = &nodeState{
+ object: nil, // not storing the object as we won't retrieve it later.
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this license node.
+ // We have a cyclic dependency!
+ return nil, errors.New("Couldn't parse license: found a cyclic dependency on " + node.ID)
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[node.ID].Color = GREY
+
+ // setting state color to black when we're done parsing this node.
+ defer func() { parser.cache[node.ID].Color = BLACK }()
+
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ if len(associatedTriples) == 0 {
+ // just a license uri string was found.
+ return parser.getSpecialLicenseFromNode(node)
+ }
+
+ // we have some attributes associated with the license node.
+ nodeType, err := getNodeTypeFromTriples(associatedTriples, node)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license triple: %v", err)
+ }
+ switch nodeType {
+ case SPDX_DISJUNCTIVE_LICENSE_SET:
+ return parser.getDisjunctiveLicenseSetFromNode(node)
+ case SPDX_CONJUNCTIVE_LICENSE_SET:
+ return parser.getConjunctiveLicenseSetFromNode(node)
+ case SPDX_EXTRACTED_LICENSING_INFO:
+ return parser.getExtractedLicensingInfoFromNode(node)
+ case SPDX_LISTED_LICENSE, SPDX_LICENSE:
+ return parser.getLicenseFromNode(node)
+ case SPDX_WITH_EXCEPTION_OPERATOR:
+ return parser.getWithExceptionOperatorFromNode(node)
+ case SPDX_OR_LATER_OPERATOR:
+ return parser.getOrLaterOperatorFromNode(node)
+ case SPDX_SIMPLE_LICENSING_INFO:
+ return parser.getSimpleLicensingInfoFromNode(node)
+ }
+ return nil, fmt.Errorf("Unknown subTag (%s) found while parsing AnyLicense", nodeType)
+}
+
+func (parser *rdfParser2_3) getLicenseExceptionFromNode(node *gordfParser.Node) (exception LicenseException, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ for _, triple := range associatedTriples {
+ value := triple.Object.ID
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_LICENSE_EXCEPTION_ID:
+ exception.licenseExceptionId = value
+ case SPDX_LICENSE_EXCEPTION_TEXT:
+ exception.licenseExceptionText = value
+ case RDFS_SEE_ALSO:
+ if !isUriValid(value) {
+ return exception, fmt.Errorf("invalid uri (%s) for seeAlso attribute of LicenseException", value)
+ }
+ exception.seeAlso = value
+ case SPDX_NAME:
+ exception.name = value
+ case SPDX_EXAMPLE:
+ exception.example = value
+ case RDFS_COMMENT:
+ exception.comment = value
+ default:
+ return exception, fmt.Errorf("invalid predicate(%s) for LicenseException", triple.Predicate)
+ }
+ }
+ return exception, nil
+}
+
+func (parser *rdfParser2_3) getSimpleLicensingInfoFromNode(node *gordfParser.Node) (SimpleLicensingInfo, error) {
+ simpleLicensingTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ return parser.getSimpleLicensingInfoFromTriples(simpleLicensingTriples)
+}
+
+func (parser *rdfParser2_3) getWithExceptionOperatorFromNode(node *gordfParser.Node) (operator WithExceptionOperator, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var memberFound bool
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ if memberFound {
+ return operator,
+ fmt.Errorf("more than one member found in the WithExceptionOperator (expected only 1)")
+ }
+ memberFound = true
+ member, err := parser.getSimpleLicensingInfoFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing member of a WithExceptionOperator: %v", err)
+ }
+ operator.member = member
+ case SPDX_LICENSE_EXCEPTION:
+ operator.licenseException, err = parser.getLicenseExceptionFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing licenseException of WithExceptionOperator: %v", err)
+ }
+ default:
+ return operator, fmt.Errorf("unknown predicate (%s) for a WithExceptionOperator", triple.Predicate.ID)
+ }
+ }
+ return operator, nil
+}
+
+func (parser *rdfParser2_3) getOrLaterOperatorFromNode(node *gordfParser.Node) (operator OrLaterOperator, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ n := len(associatedTriples)
+ if n != 2 {
+ return operator, fmt.Errorf("orLaterOperator must be associated with exactly one tag. found %v triples", n-1)
+ }
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ operator.member, err = parser.getSimpleLicensingInfoFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing simpleLicensingInfo of OrLaterOperator: %v", err)
+ }
+ default:
+ return operator, fmt.Errorf("unknown predicate %s", triple.Predicate.ID)
+ }
+ }
+ return operator, nil
+}
+
+// SpecialLicense is a type of license which is not defined in any of the
+// spdx documents, it is a type of license defined for the sake of brevity.
+// It can be [NONE|NOASSERTION|LicenseRef-<string>]
+func (parser *rdfParser2_3) getSpecialLicenseFromNode(node *gordfParser.Node) (lic SpecialLicense, err error) {
+ uri := strings.TrimSpace(node.ID)
+ switch uri {
+ case SPDX_NONE_CAPS, SPDX_NONE_SMALL:
+ return SpecialLicense{
+ value: NONE,
+ }, nil
+ case SPDX_NOASSERTION_SMALL, SPDX_NOASSERTION_CAPS:
+ return SpecialLicense{
+ value: NOASSERTION,
+ }, nil
+ }
+
+ // the license is neither NONE nor NOASSERTION
+ // checking if the license is among the standardLicenses
+ licenseAbbreviation := getLastPartOfURI(uri)
+ for _, stdLicense := range AllStandardLicenseIDS() {
+ if licenseAbbreviation == stdLicense {
+ return SpecialLicense{
+ value: SpecialLicenseValue(stdLicense),
+ }, nil
+ }
+ }
+ return lic, fmt.Errorf("found a custom license uri (%s) without any associated fields", uri)
+}
+
+func (parser *rdfParser2_3) getDisjunctiveLicenseSetFromNode(node *gordfParser.Node) (DisjunctiveLicenseSet, error) {
+ licenseSet := DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{},
+ }
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ member, err := parser.getAnyLicenseFromNode(triple.Object)
+ if err != nil {
+ return licenseSet, fmt.Errorf("error parsing disjunctive license set: %v", err)
+ }
+ licenseSet.members = append(licenseSet.members, member)
+ }
+ }
+ return licenseSet, nil
+}
+
+func (parser *rdfParser2_3) getConjunctiveLicenseSetFromNode(node *gordfParser.Node) (ConjunctiveLicenseSet, error) {
+ licenseSet := ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{},
+ }
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ member, err := parser.getAnyLicenseFromNode(triple.Object)
+ if err != nil {
+ return licenseSet, fmt.Errorf("error parsing conjunctive license set: %v", err)
+ }
+ licenseSet.members = append(licenseSet.members, member)
+ default:
+ return licenseSet, fmt.Errorf("unknown subTag for ConjunctiveLicenseSet: %s", triple.Predicate.ID)
+ }
+ }
+ return licenseSet, nil
+}
+
+func (parser *rdfParser2_3) getSimpleLicensingInfoFromTriples(triples []*gordfParser.Triple) (lic SimpleLicensingInfo, err error) {
+ for _, triple := range triples {
+ switch triple.Predicate.ID {
+ case RDFS_COMMENT:
+ lic.comment = triple.Object.ID
+ case SPDX_LICENSE_ID:
+ lic.licenseID = triple.Object.ID
+ case SPDX_NAME:
+ lic.name = triple.Object.ID
+ case RDFS_SEE_ALSO:
+ if !isUriValid(triple.Object.ID) {
+ return lic, fmt.Errorf("%s is not a valid uri for seeAlso attribute of a License", triple.Object.ID)
+ }
+ lic.seeAlso = append(lic.seeAlso, triple.Object.ID)
+ case SPDX_EXAMPLE:
+ lic.example = triple.Object.ID
+ case RDF_TYPE:
+ continue
+ default:
+ return lic, fmt.Errorf("unknown predicate(%s) for simple licensing info", triple.Predicate)
+ }
+ }
+ return lic, nil
+}
+
+func (parser *rdfParser2_3) getLicenseFromNode(node *gordfParser.Node) (lic License, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var restTriples []*gordfParser.Triple
+ for _, triple := range associatedTriples {
+ value := triple.Object.ID
+ switch triple.Predicate.ID {
+ case SPDX_IS_OSI_APPROVED:
+ lic.isOsiApproved, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isOsiApproved attribute of a License: %v", err)
+ }
+ case SPDX_LICENSE_TEXT:
+ lic.licenseText = value
+ case SPDX_STANDARD_LICENSE_HEADER:
+ lic.standardLicenseHeader = value
+ case SPDX_STANDARD_LICENSE_TEMPLATE:
+ lic.standardLicenseTemplate = value
+ case SPDX_STANDARD_LICENSE_HEADER_TEMPLATE:
+ lic.standardLicenseHeaderTemplate = value
+ case SPDX_IS_DEPRECATED_LICENSE_ID:
+ lic.isDeprecatedLicenseID, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isDeprecatedLicenseId attribute of a License: %v", err)
+ }
+ case SPDX_IS_FSF_LIBRE:
+ lic.isFsfLibre, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isFsfLibre attribute of a License: %v", err)
+ }
+ default:
+ restTriples = append(restTriples, triple)
+ }
+ }
+ lic.SimpleLicensingInfo, err = parser.getSimpleLicensingInfoFromTriples(restTriples)
+ if err != nil {
+ return lic, fmt.Errorf("error setting simple licensing information of a License: %s", err)
+ }
+ return lic, nil
+}
diff --git a/rdfloader/parser2v3/parse_license_test.go b/rdfloader/parser2v3/parse_license_test.go
new file mode 100644
index 0000000..9ff2c88
--- /dev/null
+++ b/rdfloader/parser2v3/parse_license_test.go
@@ -0,0 +1,853 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "sort"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_3_getAnyLicenseFromNode(t *testing.T) {
+ // since this function is a mux, we just have to make sure that with each
+ // type of input, it is able to redirect the request to an appropriate
+ // license getter.
+
+ // TestCase 1: input node is just a node string without any associated
+ // triple (either a NONE|NOASSERTION) because for other case,
+ // the license should've been associated with other triples
+ parser, _ := parserFromBodyContent(``)
+ inputNode := &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NONE",
+ }
+ lic, err := parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a SpecialLicense
+ switch lic.(type) {
+ case SpecialLicense:
+ default:
+ t.Errorf("expected license to be of type SpecialLicense, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 2: DisjunctiveLicenseSet:
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a DisjunctiveLicenseSet
+ switch lic.(type) {
+ case DisjunctiveLicenseSet:
+ default:
+ t.Errorf("expected license to be of type DisjunctiveLicenseSet, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 3: ConjunctiveLicenseSet:
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ConjunctiveLicenseSet
+ switch lic.(type) {
+ case ConjunctiveLicenseSet:
+ default:
+ t.Errorf("expected license to be of type ConjunctiveLicenseSet, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 4: ExtractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.dev/spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[...]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ExtractedLicensingInfo
+ switch lic.(type) {
+ case ExtractedLicensingInfo:
+ default:
+ t.Errorf("expected license to be of type ExtractedLicensingInfo, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 4: ExtractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.dev/spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[...]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ExtractedLicensingInfo
+ switch lic.(type) {
+ case ExtractedLicensingInfo:
+ default:
+ t.Errorf("expected license to be of type ExtractedLicensingInfo, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 5: License
+ parser, _ = parserFromBodyContent(`
+ <spdx:License rdf:about="http://spdx.org/licenses/Apache-2.0">
+ <spdx:standardLicenseTemplate>&lt;&gt; Apache License Version 2.0, January 2004 http://www.apache.org/licenses/&lt;&gt;&lt;&gt; TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION&lt;&gt; &lt;&gt; Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. &lt;&gt; Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. &lt;&gt; Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. &lt;&gt; Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: &lt;&gt; You must give any other recipients of the Work or Derivative Works a copy of this License; and &lt;&gt; You must cause any modified files to carry prominent notices stating that You changed the files; and &lt;&gt; You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and &lt;&gt; If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. &lt;&gt; Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. &lt;&gt; Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. &lt;&gt; Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. &lt;&gt; Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. &lt;&gt; Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.&lt;&gt; END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright &lt;&gt; Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.&lt;&gt;</spdx:standardLicenseTemplate>
+ <rdfs:seeAlso>http://www.apache.org/licenses/LICENSE-2.0</rdfs:seeAlso>
+ <spdx:name>Apache License 2.0</spdx:name>
+ <spdx:licenseId>Apache-2.0</spdx:licenseId>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <rdfs:seeAlso>http://www.opensource.org/licenses/Apache-2.0</rdfs:seeAlso>
+ <spdx:licenseText>...</spdx:licenseText>
+ <spdx:standardLicenseHeader>...</spdx:standardLicenseHeader>
+ </spdx:License>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a License
+ switch lic.(type) {
+ case License:
+ default:
+ t.Errorf("expected license to be of type License, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 5: WithExceptionOperator
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:licenseException>
+ <spdx:LicenseException rdf:nodeID="A1">
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment></rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:WithExceptionOperator>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a WithExceptionOperator
+ switch lic.(type) {
+ case WithExceptionOperator:
+ default:
+ t.Errorf("expected license to be of type WithExceptionOperator, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 6: OrLaterOperator
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a OrLaterOperator
+ switch lic.(type) {
+ case OrLaterOperator:
+ default:
+ t.Errorf("expected license to be of type OrLaterOperator, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 7: checking if an unknown license raises an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:UnknownLicense>
+ <spdx:unknownTag />
+ </spdx:UnknownLicense>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getAnyLicenseFromNode(node)
+ t.Log(err)
+ if err == nil {
+ t.Errorf("should've raised an error for invalid input")
+ }
+
+ // TestCase 8: cyclic dependent license must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet rdf:about="#SPDXRef-RecursiveLicense">
+ <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ <spdx:member>
+ <spdx:ConjunctiveLicenseSet rdf:about="#SPDXRef-RecursiveLicense">
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:member rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-RecursiveLicense"/>
+ </spdx:ConjunctiveLicenseSet>
+ </spdx:member>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getAnyLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to cyclic dependent license. found %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getConjunctiveLicenseSetFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var licenseNode *gordfParser.Node
+ var license ConjunctiveLicenseSet
+
+ // TestCase 1: invalid license member
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license member, found <nil>")
+ }
+
+ // TestCase 2: invalid predicate in the licenseSet.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/CC0-1.0"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:unknownTag />
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate found")
+ }
+
+ // TestCase 3: valid example.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing licenseSet: %v", err)
+ }
+ nMembers := len(license.members)
+ if nMembers != 2 {
+ t.Errorf("expected licenseSet to have 2 members, found %d", nMembers)
+ }
+ licenseMembers := mapLicensesToStrings(license.members)
+ expectedLicenseMembers := []string{"LGPL-2.0", "Nokia"}
+ sort.Strings(licenseMembers)
+ if !reflect.DeepEqual(licenseMembers, expectedLicenseMembers) {
+ t.Errorf("expected %v, found %v", expectedLicenseMembers, licenseMembers)
+ }
+}
+
+func Test_rdfParser2_3_getDisjunctiveLicenseSetFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var licenseNode *gordfParser.Node
+ var license DisjunctiveLicenseSet
+
+ // TestCase 1: invalid license member
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license member, found <nil>")
+ }
+
+ // TestCase 2: invalid predicate in the licenseSet.
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:unknownTag />
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate found")
+ }
+
+ // TestCase 3: valid example.
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing licenseSet: %v", err)
+ }
+ nMembers := len(license.members)
+ if nMembers != 2 {
+ t.Errorf("expected licenseSet to have 2 members, found %d", nMembers)
+ }
+ licenseMembers := mapLicensesToStrings(license.members)
+ expectedLicenseMembers := []string{"LGPL-2.0", "Nokia"}
+ sort.Strings(licenseMembers)
+ if !reflect.DeepEqual(licenseMembers, expectedLicenseMembers) {
+ t.Errorf("expected %v, found %v", expectedLicenseMembers, licenseMembers)
+ }
+}
+
+func Test_rdfParser2_3_getLicenseExceptionFromNode(t *testing.T) {
+ var licenseException LicenseException
+ var err error
+ var node *gordfParser.Node
+ var parser *rdfParser2_3
+
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <rdfs:seeAlso>see-also</rdfs:seeAlso>
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment></rdfs:comment>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getLicenseExceptionFromNode(node)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid uri for rdfs:seeAlso")
+ }
+
+ // TestCase 2: invalid predicate for licenseException
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:unknown></rdfs:unknown>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getLicenseExceptionFromNode(node)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid predicate")
+ }
+
+ // TestCase 3: everything valid
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <rdfs:seeAlso rdf:resource="http://www.opensource.org/licenses/GPL-3.0"/>
+ <spdx:example>no example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:licenseExceptionText>text</spdx:licenseExceptionText>
+ <spdx:name>name</spdx:name>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ licenseException, err = parser.getLicenseExceptionFromNode(node)
+ if err != nil {
+ t.Fatalf("unexpected error while parsing a valid licenseException")
+ }
+ expectedCrossReference := "http://www.opensource.org/licenses/GPL-3.0"
+ if licenseException.seeAlso != expectedCrossReference {
+ t.Errorf("expected: %s, found: %s", expectedCrossReference, licenseException.seeAlso)
+ }
+ expectedExample := "no example"
+ if licenseException.example != expectedExample {
+ t.Errorf("expected: %s, got: %s", expectedExample, licenseException.example)
+ }
+ if licenseException.licenseExceptionId != "Libtool-exception" {
+ t.Errorf("expected: %s, got: %s", "Libtool-exception", licenseException.licenseExceptionId)
+ }
+ if licenseException.comment != "no comments" {
+ t.Errorf("expected: %s, got: %s", "no comments", licenseException.comment)
+ }
+ if licenseException.licenseExceptionText != "text" {
+ t.Errorf("expected: '%s', got: '%s'", "text", licenseException.licenseExceptionText)
+ }
+ if licenseException.name != "name" {
+ t.Errorf("expected: '%s', got: '%s'", "name", licenseException.name)
+ }
+}
+
+func Test_rdfParser2_3_getLicenseFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var license License
+ var err error
+
+ // TestCase 1: isOsiApproved is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isOsiApproved>no</spdx:isOsiApproved>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isOsiApproved should be a valid boolean type")
+ }
+
+ // TestCase 2: rdf:seeAlso not a valid uri must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <rdfs:seeAlso>uri</rdfs:seeAlso>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating invalid uri for rdfs:seeAlso")
+ }
+
+ // TestCase 3: isDeprecatedLicenseId is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isDeprecatedLicenseId>yes</spdx:isDeprecatedLicenseId>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isDeprecatedLicenseId should be a valid boolean type")
+ }
+
+ // TestCase 4: isFsfLibre is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isFsfLibre>no</spdx:isFsfLibre>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isFsfLibre should be a valid boolean type")
+ }
+
+ // TestCase 5: invalid triple for License:
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:unknown />
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("invalid predicate should've raised an error, got <nil>")
+ }
+
+ // TestCase 5: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-3.0-or-later">
+ <rdfs:seeAlso>http://www.opensource.org/licenses/GPL-3.0</rdfs:seeAlso>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <spdx:licenseText>GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007</spdx:licenseText>
+ <spdx:name>GNU General Public License v3.0 or later</spdx:name>
+ <spdx:standardLicenseHeaderTemplate>...</spdx:standardLicenseHeaderTemplate>
+ <spdx:licenseId>GPL-3.0-or-later</spdx:licenseId>
+ <rdfs:comment>This license was released: 29 June 2007</rdfs:comment>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ <spdx:standardLicenseHeader>...</spdx:standardLicenseHeader>
+ <spdx:standardLicenseTemplate>....</spdx:standardLicenseTemplate>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ expectedSeeAlso := "http://www.opensource.org/licenses/GPL-3.0"
+ if len(license.seeAlso) != 1 {
+ t.Fatalf("expected seeAlso to have 1 element, got %d", len(license.seeAlso))
+ }
+ if license.seeAlso[len(license.seeAlso)-1] != expectedSeeAlso {
+ t.Errorf("expected %s, got %s", expectedSeeAlso, license.seeAlso)
+ }
+ if license.isOsiApproved != true {
+ t.Errorf("expected %t, got %t", true, license.isOsiApproved)
+ }
+ expectedLicenseText := "GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007"
+ if license.licenseText != expectedLicenseText {
+ t.Errorf("expected %s, got %s", expectedSeeAlso, license.licenseText)
+ }
+ expectedName := "GNU General Public License v3.0 or later"
+ if license.name != expectedName {
+ t.Errorf("expected %s, got %s", expectedName, license.name)
+ }
+ expectedstdLicHeader := "..."
+ if license.standardLicenseHeader != expectedstdLicHeader {
+ t.Errorf("expected %s, got %s", expectedstdLicHeader, license.standardLicenseHeader)
+ }
+ expectedLicenseId := "GPL-3.0-or-later"
+ if expectedLicenseId != license.licenseID {
+ t.Errorf("expected %s, got %s", expectedLicenseId, license.licenseID)
+ }
+ expectedLicenseComment := "This license was released: 29 June 2007"
+ if expectedLicenseComment != license.comment {
+ t.Errorf("expected %s, got %s", expectedLicenseComment, license.comment)
+ }
+ expectedstdLicTemplate := "..."
+ if license.standardLicenseHeader != expectedstdLicTemplate {
+ t.Errorf("expected %s, got %s", expectedstdLicTemplate, license.standardLicenseTemplate)
+ }
+ expectedstdLicHeaderTemplate := "..."
+ if license.standardLicenseHeaderTemplate != expectedstdLicHeaderTemplate {
+ t.Errorf("expected %s, got %s", expectedstdLicHeaderTemplate, license.standardLicenseHeaderTemplate)
+ }
+ if license.isFsfLibre != true {
+ t.Errorf("expected %t, got %t", true, license.isFsfLibre)
+ }
+}
+
+func Test_rdfParser2_3_getOrLaterOperatorFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: more than one member in the OrLaterOperator tag must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to more than one members, got <nil>")
+ }
+
+ // TestCase 2: Invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:members>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:members>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 5: invalid member
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:invalidTag />
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error parsing invalid license member, got %v", err)
+ }
+
+ // TestCase 4: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid input: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getSimpleLicensingInfoFromNode(t *testing.T) {
+ // nothing to test. The just provides an interface to call function that
+ // uses triples to render a SimpleLicensingInfo.
+ parser, _ := parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ _, err := parser.getSimpleLicensingInfoFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getSimpleLicensingInfoFromTriples(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var license SimpleLicensingInfo
+
+ // TestCase 1: invalid rdf:seeAlso attribute
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:seeAlso>an invalid uri</rdfs:seeAlso>
+ </spdx:SimpleLicensingInfo>
+ `)
+ _, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err == nil {
+ t.Error("expected an error reporting invalid uri for rdf:seeAlso, got <nil>")
+ }
+
+ // TestCase 2: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:invalidPredicate />
+ </spdx:SimpleLicensingInfo>
+ `)
+ _, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err == nil {
+ t.Error("expected an error reporting invalid predicate, got <nil>")
+ }
+
+ // TestCase 3: valid example
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:comment>comment</rdfs:comment>
+ <spdx:licenseId>lid</spdx:licenseId>
+ <spdx:name>name</spdx:name>
+ <rdfs:seeAlso>https://opensource.org/licenses/MPL-1.0</rdfs:seeAlso>
+ <spdx:example>example</spdx:example>
+ </spdx:SimpleLicensingInfo>
+ `)
+ license, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedComment := "comment"
+ expectedLicenseId := "lid"
+ expectedName := "name"
+ expectedSeeAlso := "https://opensource.org/licenses/MPL-1.0"
+ expectedExample := "example"
+ if expectedComment != license.comment {
+ t.Errorf("expected %v, got %v", expectedComment, license.comment)
+ }
+ if expectedLicenseId != license.licenseID {
+ t.Errorf("expected %v, got %v", expectedLicenseId, license.licenseID)
+ }
+ if expectedName != license.name {
+ t.Errorf("expected %v, got %v", expectedName, license.name)
+ }
+ if len(license.seeAlso) != 1 {
+ t.Fatalf("expected seeAlso to have 1 element, found %d", len(license.seeAlso))
+ }
+ if license.seeAlso[0] != expectedSeeAlso {
+ t.Errorf("expected %v, got %v", expectedSeeAlso, license.seeAlso[0])
+ }
+ if license.example != expectedExample {
+ t.Errorf("expected %v, got %v", expectedExample, license.example)
+ }
+}
+
+func Test_rdfParser2_3_getSpecialLicenseFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var license SpecialLicense
+
+ // TestCase 1: NONE
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NONE",
+ }
+ license, err := parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "NONE" {
+ t.Errorf("expected %s, got %s", "NONE", license.value)
+ }
+
+ // TestCase 2: NOASSERTION
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NOASSERTION",
+ }
+ license, err = parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", license.value)
+ }
+
+ // TestCase 4: undefined standard license
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "https://opensource.org/licenses/unknown",
+ }
+ _, err = parser.getSpecialLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license")
+ }
+
+ // TestCase 4: valid standard license
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "https://opensource.org/licenses/MPL-1.0",
+ }
+ license, err = parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "MPL-1.0" {
+ t.Errorf("expected %s, got %s", "MPL-1.0", license.value)
+ }
+}
+
+func Test_rdfParser2_3_getWithExceptionOperatorFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: more than one member in the OrLaterOperator tag must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to more than one members, got <nil>")
+ }
+
+ // TestCase 2: Invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:members>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:members>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 3: Invalid member
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later">
+ <spdx:unknownTag />
+ </spdx:License>
+ </spdx:member>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to error parsing a member, got <nil>")
+ }
+
+ // TestCase 4: Invalid licenseException
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:member>
+ <spdx:licenseException>
+ <spdx:LicenseException>
+ <spdx:invalidTag />
+ <spdx:example>example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid licenseException, got <nil>")
+ }
+
+ // TestCase 5: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:member>
+ <spdx:licenseException>
+ <spdx:LicenseException>
+ <spdx:example>example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_other_license_info.go b/rdfloader/parser2v3/parse_other_license_info.go
new file mode 100644
index 0000000..3e28979
--- /dev/null
+++ b/rdfloader/parser2v3/parse_other_license_info.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) getExtractedLicensingInfoFromNode(node *gordfParser.Node) (lic ExtractedLicensingInfo, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var restTriples []*gordfParser.Triple
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case SPDX_EXTRACTED_TEXT:
+ lic.extractedText = triple.Object.ID
+ default:
+ restTriples = append(restTriples, triple)
+ }
+ }
+ lic.SimpleLicensingInfo, err = parser.getSimpleLicensingInfoFromTriples(restTriples)
+ if err != nil {
+ return lic, fmt.Errorf("error setting simple licensing information of extracted licensing info: %s", err)
+ }
+ return lic, nil
+}
+
+func (parser *rdfParser2_3) extractedLicenseToOtherLicense(extLicense ExtractedLicensingInfo) (othLic v2_3.OtherLicense) {
+ othLic.LicenseIdentifier = extLicense.licenseID
+ othLic.ExtractedText = extLicense.extractedText
+ othLic.LicenseComment = extLicense.comment
+ othLic.LicenseCrossReferences = extLicense.seeAlso
+ othLic.LicenseName = extLicense.name
+ return othLic
+}
diff --git a/rdfloader/parser2v3/parse_other_license_info_test.go b/rdfloader/parser2v3/parse_other_license_info_test.go
new file mode 100644
index 0000000..cb12181
--- /dev/null
+++ b/rdfloader/parser2v3/parse_other_license_info_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_3_getExtractedLicensingInfoFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseID>LicenseRef-Freeware</spdx:licenseID>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExtractedLicensingInfoFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate, got <nil>")
+ }
+
+ // TestCase 2: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExtractedLicensingInfoFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_extractedLicenseToOtherLicense(t *testing.T) {
+ // nothing to test for this function.
+ parser, _ := parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ extLicense, _ := parser.getExtractedLicensingInfoFromNode(node)
+ othLic := parser.extractedLicenseToOtherLicense(extLicense)
+
+ if othLic.LicenseIdentifier != extLicense.licenseID {
+ t.Errorf("expected %v, got %v", othLic.LicenseIdentifier, extLicense.licenseID)
+ }
+ if othLic.ExtractedText != extLicense.extractedText {
+ t.Errorf("expected %v, got %v", othLic.ExtractedText, extLicense.extractedText)
+ }
+ if othLic.LicenseComment != extLicense.comment {
+ t.Errorf("expected %v, got %v", othLic.LicenseComment, extLicense.comment)
+ }
+ if !reflect.DeepEqual(othLic.LicenseCrossReferences, extLicense.seeAlso) {
+ t.Errorf("expected %v, got %v", othLic.LicenseCrossReferences, extLicense.seeAlso)
+ }
+ if othLic.LicenseName != extLicense.name {
+ t.Errorf("expected %v, got %v", othLic.LicenseName, extLicense.name)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_package.go b/rdfloader/parser2v3/parse_package.go
new file mode 100644
index 0000000..f455fe9
--- /dev/null
+++ b/rdfloader/parser2v3/parse_package.go
@@ -0,0 +1,362 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) getPackageFromNode(packageNode *gordfParser.Node) (pkg *v2_3.Package, err error) {
+ pkg = &v2_3.Package{} // new package which will be returned
+
+ currState := parser.cache[packageNode.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[packageNode.ID] = &nodeState{
+ object: pkg,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this package node and we needn't parse it again.
+ return currState.object.(*v2_3.Package), nil
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[packageNode.ID].Color = GREY
+
+ // setting state color to black to indicate when we're done parsing this node.
+ defer func() { parser.cache[packageNode.ID].Color = BLACK }()
+
+ // setting the SPDXIdentifier for the package.
+ eId, err := ExtractElementID(getLastPartOfURI(packageNode.ID))
+ if err != nil {
+ return nil, fmt.Errorf("error extracting elementID of a package identifier: %v", err)
+ }
+ pkg.PackageSPDXIdentifier = eId // 3.2
+
+ // check if we already have a package initialized for this ID
+ existingPackageIndex := -1
+ for ii, existingPkg := range parser.doc.Packages {
+ if existingPkg != nil && existingPkg.PackageSPDXIdentifier == eId {
+ existingPackageIndex = ii
+ pkg = existingPkg
+ break
+ }
+ }
+
+ // iterate over all the triples associated with the provided package packageNode.
+ for _, subTriple := range parser.nodeToTriples(packageNode) {
+ switch subTriple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case SPDX_NAME: // 7.1
+ // cardinality: exactly 1
+ pkg.PackageName = subTriple.Object.ID
+ case SPDX_VERSION_INFO: // 7.3
+ // cardinality: max 1
+ pkg.PackageVersion = subTriple.Object.ID
+ case SPDX_PACKAGE_FILE_NAME: // 7.4
+ // cardinality: max 1
+ pkg.PackageFileName = subTriple.Object.ID
+ case SPDX_SUPPLIER: // 7.5
+ // cardinality: max 1
+ err = setPackageSupplier(pkg, subTriple.Object.ID)
+ case SPDX_ORIGINATOR: // 7.6
+ // cardinality: max 1
+ err = setPackageOriginator(pkg, subTriple.Object.ID)
+ case SPDX_DOWNLOAD_LOCATION: // 7.7
+ // cardinality: exactly 1
+ err = setDocumentLocationFromURI(pkg, subTriple.Object.ID)
+ case SPDX_FILES_ANALYZED: // 7.8
+ // cardinality: max 1
+ err = setFilesAnalyzed(pkg, subTriple.Object.ID)
+ case SPDX_PACKAGE_VERIFICATION_CODE: // 7.9
+ // cardinality: max 1
+ err = parser.setPackageVerificationCode(pkg, subTriple.Object)
+ case SPDX_CHECKSUM: // 7.10
+ // cardinality: min 0
+ err = parser.setPackageChecksum(pkg, subTriple.Object)
+ case DOAP_HOMEPAGE: // 7.11
+ // cardinality: max 1
+ // homepage must be a valid Uri
+ if !isUriValid(subTriple.Object.ID) {
+ return nil, fmt.Errorf("invalid uri %s while parsing doap_homepage in a package", subTriple.Object.ID)
+ }
+ pkg.PackageHomePage = subTriple.Object.ID
+ case SPDX_SOURCE_INFO: // 7.12
+ // cardinality: max 1
+ pkg.PackageSourceInfo = subTriple.Object.ID
+ case SPDX_LICENSE_CONCLUDED: // 7.13
+ // cardinality: exactly 1
+ anyLicenseInfo, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ pkg.PackageLicenseConcluded = anyLicenseInfo.ToLicenseString()
+ case SPDX_LICENSE_INFO_FROM_FILES: // 7.14
+ // cardinality: min 0
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, getLicenseStringFromURI(subTriple.Object.ID))
+ case SPDX_LICENSE_DECLARED: // 7.15
+ // cardinality: exactly 1
+ anyLicenseInfo, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ pkg.PackageLicenseDeclared = anyLicenseInfo.ToLicenseString()
+ case SPDX_LICENSE_COMMENTS: // 7.16
+ // cardinality: max 1
+ pkg.PackageLicenseComments = subTriple.Object.ID
+ case SPDX_COPYRIGHT_TEXT: // 7.17
+ // cardinality: exactly 1
+ pkg.PackageCopyrightText = subTriple.Object.ID
+ case SPDX_SUMMARY: // 7.18
+ // cardinality: max 1
+ pkg.PackageSummary = subTriple.Object.ID
+ case SPDX_DESCRIPTION: // 7.19
+ // cardinality: max 1
+ pkg.PackageDescription = subTriple.Object.ID
+ case RDFS_COMMENT: // 7.20
+ // cardinality: max 1
+ pkg.PackageComment = subTriple.Object.ID
+ case SPDX_EXTERNAL_REF: // 7.21
+ // cardinality: min 0
+ externalDocRef, err := parser.getPackageExternalRef(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing externalRef of a package: %v", err)
+ }
+ pkg.PackageExternalReferences = append(pkg.PackageExternalReferences, externalDocRef)
+ case SPDX_HAS_FILE: // 7.22
+ // cardinality: min 0
+ file, err := parser.getFileFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error setting file inside a package: %v", err)
+ }
+ parser.setFileToPackage(pkg, file)
+ case SPDX_PRIMARY_PACKAGE_PURPOSE: // 7.24
+ // cardinality: exactly 1
+ pkg.PrimaryPackagePurpose = getPrimaryPackagePurpose(subTriple.Object.ID)
+ case SPDX_RELEASE_DATE: // 7.25
+ // cardinality: exactly 1
+ pkg.ReleaseDate = subTriple.Object.ID
+ case SPDX_BUILT_DATE: // 7.26
+ // cardinality: exactly 1
+ pkg.BuiltDate = subTriple.Object.ID
+ case SPDX_VALID_UNTIL_DATE: // 7.27
+ // cardinality: exactly 1
+ pkg.ValidUntilDate = subTriple.Object.ID
+ case SPDX_RELATIONSHIP:
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ case SPDX_ATTRIBUTION_TEXT:
+ // cardinality: min 0
+ pkg.PackageAttributionTexts = append(pkg.PackageAttributionTexts, subTriple.Object.ID)
+ case SPDX_ANNOTATION:
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ default:
+ return nil, fmt.Errorf("unknown predicate id %s while parsing a package", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if existingPackageIndex != -1 {
+ parser.doc.Packages[existingPackageIndex] = pkg
+ } else {
+ parser.doc.Packages = append(parser.doc.Packages, pkg)
+ }
+
+ return pkg, nil
+}
+
+// parses externalReference found in the package by the associated triple.
+func (parser *rdfParser2_3) getPackageExternalRef(node *gordfParser.Node) (externalDocRef *v2_3.PackageExternalReference, err error) {
+ externalDocRef = &v2_3.PackageExternalReference{}
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_REFERENCE_CATEGORY:
+ // cardinality: exactly 1
+ switch triple.Object.ID {
+ case SPDX_REFERENCE_CATEGORY_SECURITY:
+ externalDocRef.Category = "SECURITY"
+ case SPDX_REFERENCE_CATEGORY_PACKAGE_MANAGER:
+ externalDocRef.Category = "PACKAGE-MANAGER"
+ case SPDX_REFERENCE_CATEGORY_OTHER:
+ externalDocRef.Category = "OTHER"
+ default:
+ return nil, fmt.Errorf("unknown packageManager uri %s", triple.Predicate.ID)
+ }
+ case RDF_TYPE:
+ continue
+ case SPDX_REFERENCE_TYPE:
+ // assumes: the reference type is associated with just the uri and
+ // other associated fields are ignored.
+ // other fields include:
+ // 1. contextualExample,
+ // 2. documentation and,
+ // 3. externalReferenceSite
+ externalDocRef.RefType = triple.Object.ID
+ case SPDX_REFERENCE_LOCATOR:
+ // cardinality: exactly 1
+ externalDocRef.Locator = triple.Object.ID
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ externalDocRef.ExternalRefComment = triple.Object.ID
+ default:
+ return nil, fmt.Errorf("unknown package external reference predicate id %s", triple.Predicate.ID)
+ }
+ }
+ return
+}
+
+func getPrimaryPackagePurpose(purpose string) string {
+ value := strings.ReplaceAll(purpose, "packagePurpose_", "")
+ value = strings.ReplaceAll(value, "_", "-")
+ value = strings.ToUpper(value)
+ switch value {
+ case "APPLICATION", "FRAMEWORK", "LIBRARY", "CONTAINER", "OPERATING-SYSTEM", "DEVICE", "FIRMWARE", "SOURCE", "ARCHIVE", "FILE", "INSTALL", "OTHER":
+ return value
+ }
+ // invalid value
+ return ""
+}
+
+func (parser *rdfParser2_3) setPackageVerificationCode(pkg *v2_3.Package, node *gordfParser.Node) error {
+ if pkg.PackageVerificationCode == nil {
+ pkg.PackageVerificationCode = &common.PackageVerificationCode{}
+ }
+ for _, subTriple := range parser.nodeToTriples(node) {
+ switch subTriple.Predicate.ID {
+ case SPDX_PACKAGE_VERIFICATION_CODE_VALUE:
+ // cardinality: exactly 1
+ pkg.PackageVerificationCode.Value = subTriple.Object.ID
+ case SPDX_PACKAGE_VERIFICATION_CODE_EXCLUDED_FILE:
+ // cardinality: min 0
+ pkg.PackageVerificationCode.ExcludedFiles = append(pkg.PackageVerificationCode.ExcludedFiles, subTriple.Object.ID)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ default:
+ return fmt.Errorf("unparsed predicate %s", subTriple.Predicate.ID)
+ }
+ }
+ return nil
+}
+
+// appends the file to the package and also sets the assocWithPackage for the
+// file to indicate the file is associated with a package
+func (parser *rdfParser2_3) setFileToPackage(pkg *v2_3.Package, file *v2_3.File) {
+ if pkg.Files == nil {
+ pkg.Files = []*v2_3.File{}
+ }
+ pkg.Files = append(pkg.Files, file)
+ parser.assocWithPackage[file.FileSPDXIdentifier] = true
+}
+
+// given a supplierObject, sets the PackageSupplier attribute of the pkg.
+// Args:
+// value: [NOASSERTION | [Person | Organization]: string]
+func setPackageSupplier(pkg *v2_3.Package, value string) error {
+ value = strings.TrimSpace(value)
+ supplier := &common.Supplier{}
+ if strings.ToUpper(value) == "NOASSERTION" {
+ supplier.Supplier = "NOASSERTION"
+ pkg.PackageSupplier = supplier
+ return nil
+ }
+
+ subKey, subValue, err := ExtractSubs(value, ":")
+ if err != nil {
+ return fmt.Errorf("package supplier must be of the form NOASSERTION or [Person|Organization]: string. found: %s", value)
+ }
+ switch subKey {
+ case "Person", "Organization":
+ supplier.Supplier = subValue
+ supplier.SupplierType = subKey
+ default:
+ return fmt.Errorf("unknown supplier %s", subKey)
+ }
+
+ pkg.PackageSupplier = supplier
+
+ return nil
+}
+
+// given a OriginatorObject, sets the PackageOriginator attribute of the pkg.
+// Args:
+// value: [NOASSERTION | [Person | Organization]: string]
+func setPackageOriginator(pkg *v2_3.Package, value string) error {
+ value = strings.TrimSpace(value)
+ originator := &common.Originator{}
+ if strings.ToUpper(value) == "NOASSERTION" {
+ originator.Originator = "NOASSERTION"
+ pkg.PackageOriginator = originator
+ return nil
+ }
+
+ subKey, subValue, err := ExtractSubs(value, ":")
+ if err != nil {
+ return fmt.Errorf("package Originator must be of the form NOASSERTION or [Person|Organization]: string. found: %s", value)
+ }
+ switch subKey {
+ case "Person", "Organization":
+ originator.Originator = subValue
+ originator.OriginatorType = subKey
+ default:
+ return fmt.Errorf("unknown Originator %s", subKey)
+ }
+
+ pkg.PackageOriginator = originator
+
+ return nil
+}
+
+// validates the uri and sets the location if it is valid
+func setDocumentLocationFromURI(pkg *v2_3.Package, locationURI string) error {
+ switch locationURI {
+ case SPDX_NOASSERTION_CAPS, SPDX_NOASSERTION_SMALL:
+ pkg.PackageDownloadLocation = "NOASSERTION"
+ case SPDX_NONE_CAPS, SPDX_NONE_SMALL:
+ pkg.PackageDownloadLocation = "NONE"
+ default:
+ if !isUriValid(locationURI) {
+ return fmt.Errorf("%s is not a valid uri", locationURI)
+ }
+ pkg.PackageDownloadLocation = locationURI
+ }
+ return nil
+}
+
+// sets the FilesAnalyzed attribute to the given package
+// boolValue is a string of type "true" or "false"
+func setFilesAnalyzed(pkg *v2_3.Package, boolValue string) (err error) {
+ pkg.IsFilesAnalyzedTagPresent = true
+ pkg.FilesAnalyzed, err = boolFromString(boolValue)
+ return err
+}
+
+func (parser *rdfParser2_3) setPackageChecksum(pkg *v2_3.Package, node *gordfParser.Node) error {
+ checksumAlgorithm, checksumValue, err := parser.getChecksumFromNode(node)
+ if err != nil {
+ return fmt.Errorf("error getting checksum algorithm and value from %v", node)
+ }
+ if pkg.PackageChecksums == nil {
+ pkg.PackageChecksums = make([]common.Checksum, 0, 1)
+ }
+ switch checksumAlgorithm {
+ case common.MD5, common.SHA1, common.SHA256:
+ pkg.PackageChecksums = append(pkg.PackageChecksums, common.Checksum{Algorithm: checksumAlgorithm, Value: checksumValue})
+ default:
+ return fmt.Errorf("unknown checksumAlgorithm %s while parsing a package", checksumAlgorithm)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_package_test.go b/rdfloader/parser2v3/parse_package_test.go
new file mode 100644
index 0000000..cdaaced
--- /dev/null
+++ b/rdfloader/parser2v3/parse_package_test.go
@@ -0,0 +1,788 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_setPackageSupplier(t *testing.T) {
+ var err error
+
+ // TestCase 1: no assertion must set PackageSupplierNOASSERTION field to true
+ pkg := &v2_3.Package{}
+ err = setPackageSupplier(pkg, "NOASSERTION")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("PackageSupplier must've been set to NOASSERTION")
+ }
+
+ // TestCase 2: lower-case noassertion must also set the
+ // PackageSupplierNOASSERTION to true.
+ pkg = &v2_3.Package{}
+ err = setPackageSupplier(pkg, "noassertion")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("PackageSupplier must've been set to NOASSERTION")
+ }
+
+ // TestCase 3: invalid input without colon separator. must raise an error
+ pkg = &v2_3.Package{}
+ input := "string without colon separator"
+ err = setPackageSupplier(pkg, input)
+ if err == nil {
+ t.Errorf("invalid input \"%s\" didn't raise an error", input)
+ }
+
+ // TestCase 4: Valid Person
+ pkg = &v2_3.Package{}
+ personName := "Rishabh Bhatnagar"
+ input = "Person: " + personName
+ err = setPackageSupplier(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != personName {
+ t.Errorf("PackageSupplierPerson should be %s. found %s", personName, pkg.PackageSupplier.Supplier)
+ }
+
+ // TestCase 5: Valid Organization
+ pkg = &v2_3.Package{}
+ orgName := "SPDX"
+ input = "Organization: " + orgName
+ err = setPackageSupplier(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != orgName {
+ t.Errorf("PackageSupplierPerson should be %s. found %s", orgName, pkg.PackageSupplier.Supplier)
+ }
+
+ // TestCase 6: Invalid EntityType
+ pkg = &v2_3.Package{}
+ input = "InvalidEntity: entity"
+ err = setPackageSupplier(pkg, input)
+ if err == nil {
+ t.Errorf("invalid entity should've raised an error")
+ }
+}
+
+func Test_setPackageOriginator(t *testing.T) {
+ var err error
+
+ // TestCase 1: no assertion must set PackageSupplierNOASSERTION field to true
+ pkg := &v2_3.Package{}
+ err = setPackageOriginator(pkg, "NOASSERTION")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("PackageOriginator must've been set to NOASSERTION")
+ }
+
+ // TestCase 2: lower-case noassertion must also set the
+ // PackageOriginatorNOASSERTION to true.
+ pkg = &v2_3.Package{}
+ err = setPackageOriginator(pkg, "noassertion")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("PackageOriginator must've been set to NOASSERTION")
+ }
+
+ // TestCase 3: invalid input without colon separator. must raise an error
+ pkg = &v2_3.Package{}
+ input := "string without colon separator"
+ err = setPackageOriginator(pkg, input)
+ if err == nil {
+ t.Errorf("invalid input \"%s\" didn't raise an error", input)
+ }
+
+ // TestCase 4: Valid Person
+ pkg = &v2_3.Package{}
+ personName := "Rishabh Bhatnagar"
+ input = "Person: " + personName
+ err = setPackageOriginator(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != personName {
+ t.Errorf("PackageOriginatorPerson should be %s. found %s", personName, pkg.PackageOriginator.Originator)
+ }
+
+ // TestCase 5: Valid Organization
+ pkg = &v2_3.Package{}
+ orgName := "SPDX"
+ input = "Organization: " + orgName
+ err = setPackageOriginator(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != orgName {
+ t.Errorf("PackageOriginatorOrganization should be %s. found %s", orgName, pkg.PackageOriginator.Originator)
+ }
+
+ // TestCase 6: Invalid EntityType
+ pkg = &v2_3.Package{}
+ input = "InvalidEntity: entity"
+ err = setPackageOriginator(pkg, input)
+ if err == nil {
+ t.Errorf("invalid entity should've raised an error")
+ }
+}
+
+func Test_rdfParser2_3_setPackageVerificationCode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var pkg *v2_3.Package
+ var err error
+
+ // TestCase 1: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx.PackageVerificationCode>
+ <spdx:invalidPredicate />
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg = &v2_3.Package{}
+ err = parser.setPackageVerificationCode(pkg, node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 2: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx.PackageVerificationCode>
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg = &v2_3.Package{}
+ err = parser.setPackageVerificationCode(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedValue := "cbceb8b5689b75a584efe35587b5d41bd48820ce"
+ if pkg.PackageVerificationCode.Value != expectedValue {
+ t.Errorf("expected %v, got %v", expectedValue, pkg.PackageVerificationCode)
+ }
+ expectedExcludedFile := "./package.spdx"
+ if pkg.PackageVerificationCode.ExcludedFiles[0] != expectedExcludedFile {
+ t.Errorf("expected %v, got %v", expectedExcludedFile, pkg.PackageVerificationCode.ExcludedFiles)
+ }
+}
+
+func Test_rdfParser2_3_getPackageExternalRef(t *testing.T) {
+ var extRef *v2_3.PackageExternalReference
+ var err error
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid reference category
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_invalid"/>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid referenceCategory, got <nil>")
+ }
+
+ // TestCase 2: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:unknownPredicate />
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid referenceCategory, got <nil>")
+ }
+
+ // TestCase 3: valid example (referenceCategory_security)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef := &v2_3.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "SECURITY",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+
+ // TestCase 4: valid example (referenceCategory_packageManager)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_packageManager"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef = &v2_3.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "PACKAGE-MANAGER",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+
+ // TestCase 5: valid example (referenceCategory_other)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_other"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef = &v2_3.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "OTHER",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+}
+
+func Test_rdfParser2_3_getPrimaryPackagePurpose(t *testing.T) {
+ // TestCase 1: basic purpose
+ value := getPrimaryPackagePurpose("packagePurpose_container")
+ if value != "CONTAINER" {
+ t.Errorf("expected primary package purpose to be CONTAINER. got: '%s'", value)
+ }
+
+ // TestCase 2: purpose with underscore-to-dash
+ value = getPrimaryPackagePurpose("packagePurpose_operating_system")
+ if value != "OPERATING-SYSTEM" {
+ t.Errorf("expected primary package purpose to be OPERATING-SYSTEM. got: '%s'", value)
+ }
+
+ // TestCase 3: invalid purpose
+ value = getPrimaryPackagePurpose("packagePurpose_invalid")
+ if value != "" {
+ t.Errorf("expected invalid primary package purpose to be empty. got: '%s'", value)
+ }
+}
+
+func Test_rdfParser2_3_getPackageFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid elementId
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#upload2">
+ <spdx:name>time-1.9.tar.gz</spdx:name>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(missing SPDXRef- prefix), found %v", err)
+ }
+
+ // TestCase 2: Invalid License Concluded must raise an error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/IPL-3.0"/>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid license), found %v", err)
+ }
+
+ // TestCase 2: Invalid License Declared must raise an error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/licenses/IPL-3.0"/>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid license), found %v", err)
+ }
+
+ // TestCase 3: Invalid ExternalRef
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_invalid"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid externalRef), found %v", err)
+ }
+
+ // TestCase 4: invalid file must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#item8"/>
+ </spdx:hasFile>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid file), found %v", err)
+ }
+
+ // TestCase 5: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:hasFiles>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#item8"/>
+ </spdx:hasFiles>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid predicate), found %v", err)
+ }
+
+ // TestCase 6: invalid annotation must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid annotation), found %v", err)
+ }
+
+ // TestCase 6: invalid homepage must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <doap:homepage>u r i</doap:homepage>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid homepage uri), found %v", err)
+ }
+
+ // TestCase 7: Package tag declared more than once should be parsed into a single object's definition
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ yetAnotherPkgTriple := gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_PACKAGE_FILE_NAME,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.LITERAL,
+ ID: "packageFileName",
+ },
+ }
+ parser.nodeStringToTriples[node.String()] = append(parser.nodeStringToTriples[node.String()], &yetAnotherPkgTriple)
+ pkg, err := parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ // validating if all the attributes that spanned over two tags are included in the parsed package.
+ expectedID := "upload2"
+ if string(pkg.PackageSPDXIdentifier) != expectedID {
+ t.Errorf("expected package id: %s, got %s", expectedID, pkg.PackageSPDXIdentifier)
+ }
+ expectedPkgFileName := "packageFileName"
+ if expectedPkgFileName != pkg.PackageFileName {
+ t.Errorf("expected package file name: %s, got %s", expectedPkgFileName, pkg.PackageFileName)
+ }
+ expectedName := "Test Package"
+ if pkg.PackageName != expectedName {
+ t.Errorf("expected package name: %s, got %s", expectedPkgFileName, pkg.PackageName)
+ }
+
+ // TestCase 8: Checking if packages can handle cyclic dependencies:
+ // Simulating a smallest possible cycle: package related to itself.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes" />
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:versionInfo>1.1.1</spdx:versionInfo>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ // checking if both the attributes of the packages are set.
+ expectedVersionInfo := "1.1.1"
+ expectedPackageName := "Test Package"
+ if pkg.PackageVersion != expectedVersionInfo {
+ t.Errorf("Expected %s, found %s", expectedVersionInfo, pkg.PackageVersion)
+ }
+ if pkg.PackageName != expectedPackageName {
+ t.Errorf("Expected %s, found %s", expectedPackageName, pkg.PackageName)
+ }
+
+ // TestCase 9: everything valid
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ <spdx:versionInfo>1.1.1</spdx:versionInfo>
+ <spdx:packageFileName>time-1.9.tar.gz</spdx:packageFileName>
+ <spdx:supplier>Person: Jane Doe (jane.doe@example.com)</spdx:supplier>
+ <spdx:originator>Organization: SPDX</spdx:originator>
+ <spdx:downloadLocation rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:filesAnalyzed>true</spdx:filesAnalyzed>
+ <spdx:packageVerificationCode>
+ <spdx.PackageVerificationCode>
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ </spdx:packageVerificationCode>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>75068c26abbed3ad3980685bae21d7202d288317</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <spdx:sourceInfo>uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.</spdx:sourceInfo>
+ <spdx:licenseConcluded>
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ </spdx:licenseConcluded>
+ <spdx:licenseInfoFromFiles rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseComments>Other versions available for a commercial license</spdx:licenseComments>
+ <spdx:copyrightText rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:summary> Package for Testing </spdx:summary>
+ <spdx:description> Some tags are taken from other spdx autogenerated files </spdx:description>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ <spdx:hasFile rdf:resource="http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-129" />
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes" />
+ <spdx:relatedSpdxElement rdf:resource="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2" />
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:attributionText>attribution text</spdx:attributionText>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Package level annotation</rdfs:comment>
+ <spdx:annotator>Person: Package Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_setFileToPackage(t *testing.T) {
+ var pkg *v2_3.Package
+ var file *v2_3.File
+ var parser *rdfParser2_3
+
+ // TestCase 1: setting to a nil files attribute shouldn't panic.
+ parser, _ = parserFromBodyContent(``)
+ pkg = &v2_3.Package{}
+ file = &v2_3.File{}
+ parser.setFileToPackage(pkg, file)
+ if len(pkg.Files) != 1 {
+ t.Errorf("expected given package to have one file after setting, got %d", len(pkg.Files))
+ }
+ if parser.assocWithPackage[file.FileSPDXIdentifier] != true {
+ t.Errorf("given file should've been associated with a package, assocWithPackage is false")
+ }
+}
+
+func Test_rdfParser2_3_setPackageChecksum(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var pkg *v2_3.Package
+ var expectedChecksumValue string
+ var err error
+
+ // TestCase 1: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err == nil {
+ t.Error("expected an error due to invalid checksum node, got <nil>")
+ }
+
+ // TestCase 1: valid checksum algorithm which is invalid for package
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha384"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err == nil {
+ t.Error("expected an error due to invalid checksum for package, got <nil>")
+ }
+
+ // TestCase 2: valid checksum (sha1)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 3: valid checksum (sha256)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA256:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 4: valid checksum (md5)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.MD5:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+}
+
+func Test_setDocumentLocationFromURI(t *testing.T) {
+ var pkg *v2_3.Package
+ var expectedDocumentLocation, gotDocumentLocation string
+ var inputURI string
+ var err error
+
+ // TestCase 1: NOASSERTION
+ inputURI = SPDX_NOASSERTION_SMALL
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "NOASSERTION"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 2: NONE
+ inputURI = SPDX_NONE_CAPS
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "NONE"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 3: valid uri
+ inputURI = "https://www.gnu.org/software/texinfo/"
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "https://www.gnu.org/software/texinfo/"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 3: invalid uri
+ inputURI = " "
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err == nil {
+ t.Fatalf("expected an error due to invalid uri, got %v", err)
+ }
+}
+
+func Test_setFilesAnalyzed(t *testing.T) {
+ var pkg *v2_3.Package
+ var err error
+
+ // TestCase 1: not a valid bool value:
+ pkg = &v2_3.Package{}
+ err = setFilesAnalyzed(pkg, "no")
+ if err == nil {
+ t.Errorf("expected an error due to invalid bool input, got %v", err)
+ }
+
+ // TestCase 2: valid input
+ pkg = &v2_3.Package{}
+ err = setFilesAnalyzed(pkg, "true")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !pkg.IsFilesAnalyzedTagPresent {
+ t.Errorf("should've set IsFilesAnalyzedTagPresent, got: %t", pkg.IsFilesAnalyzedTagPresent)
+ }
+ if !pkg.FilesAnalyzed {
+ t.Errorf("expected: %t, got: %t", true, pkg.FilesAnalyzed)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_relationship.go b/rdfloader/parser2v3/parse_relationship.go
new file mode 100644
index 0000000..e182a45
--- /dev/null
+++ b/rdfloader/parser2v3/parse_relationship.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// parsing the relationship that exists in the rdf document.
+// Relationship is of type RefA relationType RefB.
+// parsing the relationship appends the relationship to the current document's
+// Relationships Slice.
+func (parser *rdfParser2_3) parseRelationship(triple *gordfParser.Triple) (err error) {
+ reln := v2_3.Relationship{}
+
+ reln.RefA, err = getReferenceFromURI(triple.Subject.ID)
+ if err != nil {
+ return err
+ }
+
+ currState := parser.cache[triple.Object.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[triple.Object.ID] = &nodeState{
+ object: reln,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this relationship node and we needn't parse it again.
+ return nil
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[triple.Object.ID].Color = GREY
+
+ // setting state color to black to indicate when we're done parsing this node.
+ defer func() { parser.cache[triple.Object.ID].Color = BLACK }()
+
+ for _, subTriple := range parser.nodeToTriples(triple.Object) {
+ switch subTriple.Predicate.ID {
+ case SPDX_RELATIONSHIP_TYPE:
+ // cardinality: exactly 1
+ reln.Relationship, err = getRelationshipTypeFromURI(subTriple.Object.ID)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case SPDX_RELATED_SPDX_ELEMENT:
+ // cardinality: exactly 1
+ // assumes: spdx-element is a uri
+ reln.RefB, err = getReferenceFromURI(subTriple.Object.ID)
+ if err != nil {
+ return err
+ }
+
+ relatedSpdxElementTriples := parser.nodeToTriples(subTriple.Object)
+ if len(relatedSpdxElementTriples) == 0 {
+ continue
+ }
+
+ typeTriples := rdfwriter.FilterTriples(relatedSpdxElementTriples, &subTriple.Object.ID, &RDF_TYPE, nil)
+ if len(typeTriples) != 1 {
+ return fmt.Errorf("expected %s to have exactly one rdf:type triple. found %d triples", subTriple.Object, len(typeTriples))
+ }
+ err = parser.parseRelatedElementFromTriple(&reln, typeTriples[0])
+ if err != nil {
+ return err
+ }
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ reln.RelationshipComment = subTriple.Object.ID
+ default:
+ return fmt.Errorf("unexpected predicate id: %s", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ parser.doc.Relationships = append(parser.doc.Relationships, &reln)
+ return nil
+}
+
+func (parser *rdfParser2_3) parseRelatedElementFromTriple(reln *v2_3.Relationship, triple *gordfParser.Triple) error {
+ // iterate over relatedElement Type and check which SpdxElement it is.
+ var err error
+ switch triple.Object.ID {
+ case SPDX_FILE:
+ file, err := parser.getFileFromNode(triple.Subject)
+ if err != nil {
+ return fmt.Errorf("error setting a file: %v", err)
+ }
+ reln.RefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: file.FileSPDXIdentifier,
+ }
+
+ case SPDX_PACKAGE:
+ pkg, err := parser.getPackageFromNode(triple.Subject)
+ if err != nil {
+ return fmt.Errorf("error setting a package inside a relationship: %v", err)
+ }
+ reln.RefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: pkg.PackageSPDXIdentifier,
+ }
+
+ case SPDX_SPDX_ELEMENT:
+ // it shouldn't be associated with any other triple.
+ // it must be a uri reference.
+ reln.RefB, err = ExtractDocElementID(getLastPartOfURI(triple.Subject.ID))
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("undefined relatedElement %s found while parsing relationship", triple.Object.ID)
+ }
+ return nil
+}
+
+// references like RefA and RefB of any relationship
+func getReferenceFromURI(uri string) (common.DocElementID, error) {
+ fragment := getLastPartOfURI(uri)
+ switch strings.ToLower(strings.TrimSpace(fragment)) {
+ case "noassertion", "none":
+ return common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: common.ElementID(strings.ToUpper(fragment)),
+ }, nil
+ }
+ return ExtractDocElementID(fragment)
+}
+
+// note: relationshipType is case sensitive.
+func getRelationshipTypeFromURI(relnTypeURI string) (string, error) {
+ relnTypeURI = strings.TrimSpace(relnTypeURI)
+ lastPart := getLastPartOfURI(relnTypeURI)
+ if !strings.HasPrefix(lastPart, PREFIX_RELATIONSHIP_TYPE) {
+ return "", fmt.Errorf("relationshipType must start with %s. found %s", PREFIX_RELATIONSHIP_TYPE, lastPart)
+ }
+ lastPart = strings.TrimPrefix(lastPart, PREFIX_RELATIONSHIP_TYPE)
+
+ lastPart = strings.TrimSpace(lastPart)
+ for _, validRelationshipType := range AllRelationshipTypes() {
+ if lastPart == validRelationshipType {
+ return lastPart, nil
+ }
+ }
+ return "", fmt.Errorf("unknown relationshipType: '%s'", lastPart)
+}
diff --git a/rdfloader/parser2v3/parse_relationship_test.go b/rdfloader/parser2v3/parse_relationship_test.go
new file mode 100644
index 0000000..9c4ddff
--- /dev/null
+++ b/rdfloader/parser2v3/parse_relationship_test.go
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_getReferenceFromURI(t *testing.T) {
+ // TestCase 1: noassertion uri
+ ref, err := getReferenceFromURI(SPDX_NOASSERTION_CAPS)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "NOASSERTION" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "NOASSERTION")
+ }
+
+ // TestCase 2: NONE uri
+ ref, err = getReferenceFromURI(SPDX_NONE_CAPS)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "NONE" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "NONE")
+ }
+
+ // TestCase 3: Valid URI
+ ref, err = getReferenceFromURI(NS_SPDX + "SPDXRef-item1")
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "item1" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "item1")
+ }
+
+ // TestCase 3: Invalid URI
+ _, err = getReferenceFromURI(NS_SPDX + "item1")
+ if err == nil {
+ t.Errorf("should've raised an error for invalid input")
+ }
+}
+
+func Test_getRelationshipTypeFromURI(t *testing.T) {
+ // TestCase 1: valid relationshipType
+ relnType := "expandedFromArchive"
+ op, err := getRelationshipTypeFromURI(NS_SPDX + "relationshipType_" + relnType)
+ if err != nil {
+ t.Errorf("error getting relationship type from a valid input")
+ }
+ if op != relnType {
+ t.Errorf("expected %s, found %s", relnType, op)
+ }
+
+ // TestCase2: invalid relationshipType
+ relnType = "invalidRelationship"
+ _, err = getRelationshipTypeFromURI(NS_SPDX + "relationshipType_" + relnType)
+ if err == nil {
+ t.Errorf("should've raised an error for an invalid input(%s)", relnType)
+ }
+}
+
+func Test_rdfParser2_3_parseRelatedElementFromTriple(t *testing.T) {
+ // TestCase 1: Package as a related element
+ parser, _ := parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln := &v2_3.Relationship{}
+ triple := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)[0]
+ err := parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 3: invalid package as a relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Package id, got %v", err)
+ }
+
+ // TestCase 4: valid File as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 5: invalid File as a relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error while parsing an invalid File, got %v", err)
+ }
+
+ // TestCase 6: valid SpdxElement as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_SPDX_ELEMENT)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "File",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 7: invalid SpdxElement as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-:File"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_SPDX_ELEMENT)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid documentId for SpdxElement, got %v", err)
+ }
+}
+
+func Test_rdfParser2_3_parseRelationship(t *testing.T) {
+ // TestCase 1: invalid RefA
+ parser, _ := parserFromBodyContent(`
+ <spdx:File>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err := parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid RefA")
+ }
+
+ // TestCase 3: invalid RefB
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid RefB")
+ }
+
+ // TestCase 3: more than one typeTriple for relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <spdx:relatedSpdxElement>
+ <spdx:File/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to more than one type triples")
+ }
+
+ // TestCase 4: undefined relatedSpdxElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Unknown rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to unknown relatedElement, got %v", err)
+ }
+
+ // TestCase 6: relatedElement associated with more than one type
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid relatedElement, got %v", err)
+ }
+
+ // TestCase 5: unknown predicate inside a relationship
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Unknown rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <spdx:unknownPredicate/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to unknown predicate in a relationship")
+ }
+
+ // TestCase 8: Recursive relationships mustn't raise any error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship rdf:about="#SPDXRef-reln">
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon">
+ <spdx:relationship>
+ <spdx:Relationship rdf:about="#SPDXRef-reln">
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+
+ // TestCase 7: completely valid example:
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid relationship: %v", err)
+ }
+ // validating parsed attributes
+ if len(parser.doc.Relationships) != 1 {
+ t.Errorf("after parsing a valid relationship, doc should've had 1 relationship, found %d", len(parser.doc.Relationships))
+ }
+ reln := parser.doc.Relationships[0]
+ expectedRelnType := "describes"
+ if reln.Relationship != expectedRelnType {
+ t.Errorf("expected %s, found %s", expectedRelnType, reln.Relationship)
+ }
+ expectedRefA := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "File",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+ expectedComment := "comment"
+ if reln.RelationshipComment != expectedComment {
+ t.Errorf("expected %v, found %v", expectedComment, reln.RelationshipComment)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_review.go b/rdfloader/parser2v3/parse_review.go
new file mode 100644
index 0000000..c1c8b02
--- /dev/null
+++ b/rdfloader/parser2v3/parse_review.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) setReviewFromNode(reviewedNode *gordfParser.Node) error {
+ review := v2_3.Review{}
+ for _, triple := range parser.nodeToTriples(reviewedNode) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ review.ReviewComment = triple.Object.ID
+ case SPDX_REVIEW_DATE:
+ // cardinality: exactly 1
+ review.ReviewDate = triple.Object.ID
+ case SPDX_REVIEWER:
+ // cardinality: max 1
+ var err error
+ review.ReviewerType, review.Reviewer, err = ExtractSubs(triple.Object.ID, ":")
+ if err != nil {
+ return fmt.Errorf("error parsing reviewer: %v", err)
+ }
+ default:
+ return fmt.Errorf("unknown predicate %v for review triples", triple.Predicate)
+ }
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, &review)
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_review_test.go b/rdfloader/parser2v3/parse_review_test.go
new file mode 100644
index 0000000..18818aa
--- /dev/null
+++ b/rdfloader/parser2v3/parse_review_test.go
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+)
+
+func Test_rdfParser2_3_setReviewFromNode(t *testing.T) {
+ // TestCase 1: unknown predicate must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ <spdx:unknown />
+ </spdx:Review>
+ `)
+ reviewNode := parser.gordfParserObj.Triples[0].Subject
+ err := parser.setReviewFromNode(reviewNode)
+ if err == nil {
+ t.Errorf("unknown predicate should've elicit an error")
+ }
+
+ // TestCase 2: wrong reviewer format must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ `)
+ reviewNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setReviewFromNode(reviewNode)
+ if err == nil {
+ t.Errorf("incorrect should've elicit an error")
+ }
+
+ // TestCase 3: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne</spdx:reviewer>
+ </spdx:Review>
+ `)
+ reviewNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setReviewFromNode(reviewNode)
+ if err != nil {
+ t.Errorf("error parsing a valid node")
+ }
+ n := len(parser.doc.Reviews)
+ if n != 1 {
+ t.Errorf("expected doc to have 1 review, found %d", n)
+ }
+ review := parser.doc.Reviews[0]
+ expectedComment := "Another example reviewer."
+ if review.ReviewComment != expectedComment {
+ t.Errorf("expected: %v, found: %s", expectedComment, review.ReviewComment)
+ }
+ expectedDate := "2011-03-13T00:00:00Z"
+ if review.ReviewDate != expectedDate {
+ t.Errorf("expected %s, found %s", expectedDate, review.ReviewDate)
+ }
+ expectedReviewer := "Suzanne"
+ if review.Reviewer != expectedReviewer {
+ t.Errorf("expected %s, found %s", expectedReviewer, review.Reviewer)
+ }
+ expectedReviewerType := "Person"
+ if review.ReviewerType != expectedReviewerType {
+ t.Errorf("expected %s, found %s", expectedReviewerType, review.ReviewerType)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_snippet_info.go b/rdfloader/parser2v3/parse_snippet_info.go
new file mode 100644
index 0000000..90da873
--- /dev/null
+++ b/rdfloader/parser2v3/parse_snippet_info.go
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strconv"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// Snippet Information
+// Cardinality: Optional, Many
+func (parser *rdfParser2_3) getSnippetInformationFromNode2_3(node *gordfParser.Node) (si *v2_3.Snippet, err error) {
+ si = &v2_3.Snippet{}
+
+ err = setSnippetID(node.ID, si)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, siTriple := range parser.nodeToTriples(node) {
+ switch siTriple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ case SPDX_SNIPPET_FROM_FILE:
+ // cardinality: exactly 1
+ // file which is associated with the snippet
+ _, err := parser.getFileFromNode(siTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ docElemID, err := ExtractDocElementID(getLastPartOfURI(siTriple.Object.ID))
+ si.SnippetFromFileSPDXIdentifier = docElemID.ElementRefID
+ case SPDX_RANGE:
+ // cardinality: min 1
+ err = parser.setSnippetRangeFromNode(siTriple.Object, si)
+ if err != nil {
+ return nil, err
+ }
+ case SPDX_LICENSE_INFO_IN_SNIPPET:
+ // license info in snippet can be NONE, NOASSERTION or SimpleLicensingInfo
+ // using AnyLicenseInfo because it can redirect the request and
+ // can handle NONE & NOASSERTION
+ var anyLicense AnyLicenseInfo
+ anyLicense, err = parser.getAnyLicenseFromNode(siTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license info in snippet: %v", err)
+ }
+ si.LicenseInfoInSnippet = append(si.LicenseInfoInSnippet, anyLicense.ToLicenseString())
+ case SPDX_NAME:
+ si.SnippetName = siTriple.Object.ID
+ case SPDX_COPYRIGHT_TEXT:
+ si.SnippetCopyrightText = siTriple.Object.ID
+ case SPDX_LICENSE_COMMENTS:
+ si.SnippetLicenseComments = siTriple.Object.ID
+ case RDFS_COMMENT:
+ si.SnippetComment = siTriple.Object.ID
+ case SPDX_LICENSE_CONCLUDED:
+ var anyLicense AnyLicenseInfo
+ anyLicense, err = parser.getAnyLicenseFromNode(siTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license info in snippet: %v", err)
+ }
+ si.SnippetLicenseConcluded = anyLicense.ToLicenseString()
+ default:
+ return nil, fmt.Errorf("unknown predicate %v", siTriple.Predicate.ID)
+ }
+ }
+ return si, nil
+}
+
+// given is the id of the file, sets the snippet to the file in parser.
+func (parser *rdfParser2_3) setSnippetToFileWithID(snippet *v2_3.Snippet, fileID common.ElementID) error {
+ if parser.files[fileID] == nil {
+ return fmt.Errorf("snippet refers to an undefined file with ID: %s", fileID)
+ }
+
+ // initializing snippet of the files if it is not defined already
+ if parser.files[fileID].Snippets == nil {
+ parser.files[fileID].Snippets = map[common.ElementID]*v2_3.Snippet{}
+ }
+
+ // setting the snippet to the file.
+ parser.files[fileID].Snippets[snippet.SnippetSPDXIdentifier] = snippet
+
+ return nil
+}
+
+func (parser *rdfParser2_3) setSnippetRangeFromNode(node *gordfParser.Node, si *v2_3.Snippet) error {
+ // for a range object, we can have only 3 associated triples:
+ // node -> RDF_TYPE -> Object
+ // node -> startPointer -> Object
+ // node -> endPointer -> Object
+ associatedTriples := parser.nodeToTriples(node)
+ if len(associatedTriples) != 3 {
+ return fmt.Errorf("range should be associated with exactly 3 triples, got %d", len(associatedTriples))
+ }
+
+ // Triple 1: Predicate=RDF_TYPE
+ typeTriple := rdfwriter.FilterTriples(associatedTriples, &node.ID, &RDF_TYPE, nil)
+ if len(typeTriple) != 1 {
+ // we had 3 associated triples. out of which 2 is start and end pointer,
+ // if we do not have the rdf:type triple as the third one,
+ // we have either extra or undefined predicate.
+ return fmt.Errorf("every object node must be associated with exactly one rdf:type triple, found: %d", len(typeTriple))
+ }
+
+ // getting start pointer
+ startPointerTriples := rdfwriter.FilterTriples(associatedTriples, &node.ID, &PTR_START_POINTER, nil)
+ if len(startPointerTriples) != 1 {
+ return fmt.Errorf("range object must be associated with exactly 1 startPointer, got %d", len(startPointerTriples))
+ }
+ startRangeType, start, err := parser.getPointerFromNode(startPointerTriples[0].Object, si)
+ if err != nil {
+ return fmt.Errorf("error parsing startPointer: %v", err)
+ }
+
+ // getting end pointer
+ endPointerTriples := rdfwriter.FilterTriples(associatedTriples, &node.ID, &PTR_END_POINTER, nil)
+ if len(startPointerTriples) != 1 {
+ return fmt.Errorf("range object must be associated with exactly 1 endPointer, got %d", len(endPointerTriples))
+ }
+ endRangeType, end, err := parser.getPointerFromNode(endPointerTriples[0].Object, si)
+ if err != nil {
+ return fmt.Errorf("error parsing endPointer: %v", err)
+ }
+
+ // return error when start and end pointer type is not same.
+ if startRangeType != endRangeType {
+ return fmt.Errorf("start and end range type doesn't match")
+ }
+
+ si.Ranges = []common.SnippetRange{{
+ StartPointer: common.SnippetRangePointer{FileSPDXIdentifier: si.SnippetFromFileSPDXIdentifier},
+ EndPointer: common.SnippetRangePointer{FileSPDXIdentifier: si.SnippetFromFileSPDXIdentifier},
+ }}
+
+ if startRangeType == LINE_RANGE {
+ si.Ranges[0].StartPointer.LineNumber = start
+ si.Ranges[0].EndPointer.LineNumber = end
+ } else {
+ si.Ranges[0].StartPointer.Offset = start
+ si.Ranges[0].EndPointer.Offset = end
+ }
+ return nil
+}
+
+func (parser *rdfParser2_3) getPointerFromNode(node *gordfParser.Node, si *v2_3.Snippet) (rt RangeType, number int, err error) {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ case PTR_REFERENCE:
+ err = parser.parseRangeReference(triple.Object, si)
+ case PTR_OFFSET:
+ number, err = strconv.Atoi(triple.Object.ID)
+ rt = BYTE_RANGE
+ case PTR_LINE_NUMBER:
+ number, err = strconv.Atoi(triple.Object.ID)
+ rt = LINE_RANGE
+ default:
+ err = fmt.Errorf("undefined predicate (%s) for a pointer", triple.Predicate)
+ }
+ if err != nil {
+ return
+ }
+ }
+ if rt == "" {
+ err = fmt.Errorf("range type not defined for a pointer")
+ }
+ return
+}
+
+func (parser *rdfParser2_3) parseRangeReference(node *gordfParser.Node, snippet *v2_3.Snippet) error {
+ // reference is supposed to be either a resource reference to an already
+ // defined or a new file. Unfortunately, I didn't find field where this can be set in the tools-golang data model.
+ // todo: set this reference to the snippet
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ if len(associatedTriples) == 0 {
+ return nil
+ }
+ _, err := parser.getFileFromNode(node)
+ if err != nil {
+ return fmt.Errorf("error parsing a new file in a reference: %v", err)
+ }
+ return nil
+}
+
+func setSnippetID(uri string, si *v2_3.Snippet) (err error) {
+ fragment := getLastPartOfURI(uri)
+ si.SnippetSPDXIdentifier, err = ExtractElementID(fragment)
+ if err != nil {
+ return fmt.Errorf("error setting snippet identifier: %v", uri)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_snippet_info_test.go b/rdfloader/parser2v3/parse_snippet_info_test.go
new file mode 100644
index 0000000..2147586
--- /dev/null
+++ b/rdfloader/parser2v3/parse_snippet_info_test.go
@@ -0,0 +1,536 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_rdfParser2_3_getSnippetInformationFromTriple2_3(t *testing.T) {
+ var err error
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid snippet id:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#Snippet">
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid, got %v", err)
+ }
+
+ // TestCase 2: Invalid LicenseInfoInSnippet
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/licenses/Unknown"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid licenseInfoInSnippet, got %v", err)
+ }
+
+ // TestCase 3: Invalid range.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:range>
+ <spdx:StartEndPointer>
+ <spdx:unknownTag />
+ </spdx:StartEndPointer>
+ </spdx:range>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid range, got %v", err)
+ }
+
+ // TestCase 3: invalid file in snippetFromFile
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:snippetFromFile>
+ <spdx:File rdf:resource="http://anupam-VirtualBox/spdx.rdf#item8" />
+ </spdx:snippetFromFile>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid snippetFromFile, got %v", err)
+ }
+
+ // TestCase 4: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:unknownPredicate />
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, got %v", err)
+ }
+
+ // TestCase 5: invalid license concluded:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/Unknown"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid licenseConcluded, got %v", err)
+ }
+
+ // TestCase 6: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:snippetFromFile>
+ <spdx:File rdf:about="#SPDXRef-File" />
+ </spdx:snippetFromFile>
+ <spdx:range>
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>420</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ </spdx:range>
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ <spdx:name>snippet test</spdx:name>
+ <spdx:copyrightText>test</spdx:copyrightText>
+ <spdx:licenseComments>comments</spdx:licenseComments>
+ <rdfs:comment>comments</rdfs:comment>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err != nil {
+ t.Fatalf("error parsing a valid example: %v", err)
+ }
+}
+
+func Test_setSnippetID(t *testing.T) {
+ // TestCase 1: invalid input (empty)
+ err := setSnippetID("", &v2_3.Snippet{})
+ if err == nil {
+ t.Errorf("should've raised an error for empty input")
+ }
+
+ // TestCase 2: valid input
+ si := &v2_3.Snippet{}
+ err = setSnippetID("http://spdx.org/spdxdocs/spdx-example#SPDXRef-Snippet", si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if si.SnippetSPDXIdentifier != "Snippet" {
+ t.Errorf("expected: %s, found: %s", "Snippet", si.SnippetSPDXIdentifier)
+ }
+}
+
+func Test_rdfParser2_3_parseRangeReference(t *testing.T) {
+ var err error
+ var node *gordfParser.Node
+ var parser *rdfParser2_3
+ var si *v2_3.Snippet
+
+ // TestCase 1: ResourceLiteral node without a new file shouldn't raise any error.
+ si = &v2_3.Snippet{}
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.RESOURCELITERAL,
+ ID: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource",
+ }
+ err = parser.parseRangeReference(node, si)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+
+ // TestCase 2: invalid file in the reference should raise an error
+ si = &v2_3.Snippet{}
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#DoapSource">
+ <spdx:fileName> test file </spdx:fileName>
+ </spdx:File>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseRangeReference(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid file in the range reference, got %v", err)
+ }
+
+ // TestCase 3: A valid reference must set the file to the files map of the parser.
+ si = &v2_3.Snippet{}
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource">
+ <spdx:fileName> test file </spdx:fileName>
+ </spdx:File>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseRangeReference(node, si)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ if len(parser.files) != 1 {
+ t.Errorf("expected parser.files to have 1 file, found %d", len(parser.files))
+ }
+}
+
+func Test_rdfParser2_3_getPointerFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var si *v2_3.Snippet
+ var err error
+ var rt RangeType
+ var number int
+
+ // TestCase 1: invalid number in the offset field must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>3-10</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid offset, got %v", err)
+ }
+
+ // TestCase 2: invalid number in the lineNumber field must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>3-10</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid offset, got %v", err)
+ }
+
+ // TestCase 3: invalid predicate in the pointer field
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <spdx:invalidTag />
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>3-10</j.0:lineNumber>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid predicate, got %v", err)
+ }
+
+ // TestCase 4: No range type defined must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid rangeType, got %v", err)
+ }
+
+ // TestCase 5: valid example
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ rt, number, err = parser.getPointerFromNode(node, si)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid node: %v", err)
+ }
+ if rt != BYTE_RANGE {
+ t.Errorf("expected: %s, got: %s", BYTE_RANGE, rt)
+ }
+ if number != 310 {
+ t.Errorf("expected: %d, got: %d", 310, number)
+ }
+}
+
+func Test_rdfParser2_3_setSnippetRangeFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var si *v2_3.Snippet
+ var node *gordfParser.Node
+
+ // TestCase 1: range with less one pointer less must raise an error
+ // (end-pointer missing in the range)
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing end pointer, got %v", err)
+ }
+
+ // TestCase 2: triples with 0 or more than one type-triple
+ parser, _ = parserFromBodyContent(`
+
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ dummyTriple := parser.gordfParserObj.Triples[0]
+ // resetting the node to be associated with 3 triples which will have
+ // rdf:type triple either thrice or 0 times.
+ parser.nodeStringToTriples[node.String()] = []*gordfParser.Triple{
+ dummyTriple, dummyTriple, dummyTriple,
+ }
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid rdf:type triples, got %v", err)
+ }
+
+ // TestCase 3: triples with 0 startPointer
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing start pointer, got %v", err)
+ }
+
+ // TestCase 4: triples with 0 endPointer
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing end pointer, got %v", err)
+ }
+
+ // TestCase 5: error parsing start pointer must be propagated to the range
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>42.0</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid start pointer, got %v", err)
+ }
+
+ // TestCase 6: error parsing end pointer must be propagated to the range
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>31+0</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid end pointer, got %v", err)
+ }
+
+ // TestCase 7: mismatching start and end pointer must also raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to mismatching start and end pointers, got %v", err)
+ }
+
+ // TestCase 8: everything valid(byte_range):
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ // TestCase 9: everything valid(line_range):
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>420</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_setSnippetToFileWithID(t *testing.T) {
+ var parser *rdfParser2_3
+ var fileId common.ElementID
+ var si *v2_3.Snippet
+ var file *v2_3.File
+ var err error
+
+ // TestCase 1: file id which is not associated with any file must raise an error.
+ parser, _ = parserFromBodyContent("")
+ si = &v2_3.Snippet{}
+ err = parser.setSnippetToFileWithID(si, fileId)
+ if err == nil {
+ t.Errorf("expected an error saying undefined file")
+ }
+
+ // TestCase 2: file exists, but snippet of the file doesn't ( it mustn't raise any error )
+ fileId = common.ElementID("File1")
+ file = &v2_3.File{
+ FileSPDXIdentifier: fileId,
+ }
+ parser.files[fileId] = file
+ file.Snippets = nil // nil snippets
+ err = parser.setSnippetToFileWithID(si, fileId)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if len(file.Snippets) != 1 {
+ t.Errorf("expected file to have 1 snippet, got %d", len(file.Snippets))
+ }
+}
diff --git a/rdfloader/parser2v3/parse_spdx_document.go b/rdfloader/parser2v3/parse_spdx_document.go
new file mode 100644
index 0000000..abf89f9
--- /dev/null
+++ b/rdfloader/parser2v3/parse_spdx_document.go
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) parseSpdxDocumentNode(spdxDocNode *gordfParser.Node) (err error) {
+ // shorthand for document's creation info.
+ ci := parser.doc.CreationInfo
+
+ // parse the document header information (SPDXID and document namespace)
+ // the Subject.ID is of type baseURI#spdxID
+ baseUri, offset, err := ExtractSubs(spdxDocNode.ID, "#")
+ if err != nil {
+ return err
+ }
+ parser.doc.DocumentNamespace = baseUri // 2.5
+ parser.doc.SPDXIdentifier = common.ElementID(offset) // 2.3
+
+ // parse other associated triples.
+ for _, subTriple := range parser.nodeToTriples(spdxDocNode) {
+ objectValue := subTriple.Object.ID
+ switch subTriple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_SPEC_VERSION: // 2.1: specVersion
+ // cardinality: exactly 1
+ parser.doc.SPDXVersion = objectValue
+ case SPDX_DATA_LICENSE: // 2.2: dataLicense
+ // cardinality: exactly 1
+ dataLicense, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.DataLicense = dataLicense.ToLicenseString()
+ case SPDX_NAME: // 2.4: DocumentName
+ // cardinality: exactly 1
+ parser.doc.DocumentName = objectValue
+ case SPDX_EXTERNAL_DOCUMENT_REF: // 2.6: externalDocumentReferences
+ // cardinality: min 0
+ var extRef v2_3.ExternalDocumentRef
+ extRef, err = parser.getExternalDocumentRefFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.ExternalDocumentReferences = append(parser.doc.ExternalDocumentReferences, extRef)
+ case SPDX_CREATION_INFO: // 2.7 - 2.10:
+ // cardinality: exactly 1
+ err = parser.parseCreationInfoFromNode(ci, subTriple.Object)
+ case RDFS_COMMENT: // 2.11: Document Comment
+ // cardinality: max 1
+ parser.doc.DocumentComment = objectValue
+ case SPDX_REVIEWED: // reviewed:
+ // cardinality: min 0
+ err = parser.setReviewFromNode(subTriple.Object)
+ case SPDX_DESCRIBES_PACKAGE: // describes Package
+ // cardinality: min 0
+ var pkg *v2_3.Package
+ pkg, err = parser.getPackageFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.Packages = append(parser.doc.Packages, pkg)
+ case SPDX_HAS_EXTRACTED_LICENSING_INFO: // hasExtractedLicensingInfo
+ // cardinality: min 0
+ extractedLicensingInfo, err := parser.getExtractedLicensingInfoFromNode(subTriple.Object)
+ if err != nil {
+ return fmt.Errorf("error setting extractedLicensingInfo in spdxDocument: %v", err)
+ }
+ othLicense := parser.extractedLicenseToOtherLicense(extractedLicensingInfo)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, &othLicense)
+ case SPDX_RELATIONSHIP: // relationship
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ case SPDX_ANNOTATION: // annotations
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ default:
+ return fmt.Errorf("invalid predicate while parsing SpdxDocument: %v", subTriple.Predicate)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (parser *rdfParser2_3) getExternalDocumentRefFromNode(node *gordfParser.Node) (edr v2_3.ExternalDocumentRef, err error) {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_EXTERNAL_DOCUMENT_ID:
+ // cardinality: exactly 1
+ edr.DocumentRefID = triple.Object.ID
+ case SPDX_SPDX_DOCUMENT:
+ // cardinality: exactly 1
+ // assumption: "spdxDocument" property of an external document
+ // reference is just a uri which doesn't follow a spdxDocument definition
+ edr.URI = triple.Object.ID
+ case SPDX_CHECKSUM:
+ // cardinality: exactly 1
+ alg, checksum, err := parser.getChecksumFromNode(triple.Object)
+ if err != nil {
+ return edr, err
+ }
+ edr.Checksum.Value = checksum
+ edr.Checksum.Algorithm = alg
+ case RDF_TYPE:
+ continue
+ default:
+ return edr, fmt.Errorf("unknown predicate ID (%s) while parsing externalDocumentReference", triple.Predicate.ID)
+ }
+ }
+ return edr, nil
+}
diff --git a/rdfloader/parser2v3/parse_spdx_document_test.go b/rdfloader/parser2v3/parse_spdx_document_test.go
new file mode 100644
index 0000000..f2c0f2f
--- /dev/null
+++ b/rdfloader/parser2v3/parse_spdx_document_test.go
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_3_getExternalDocumentRefFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid checksum, found %v", err)
+ }
+
+ // TestCase 2: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:unknownTag />
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, found %v", err)
+ }
+
+ // TestCase 3: valid example
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_parseSpdxDocumentNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid spdx id of the document
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301/Document"/>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid document id, got %v", err)
+ }
+
+ // TestCase 2: erroneous dataLicense
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:dataLicense rdf:resource="http://spdx.org/rdf/terms#Unknown" />
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid dataLicense, got %v", err)
+ }
+
+ // TestCase 3: invalid external document ref
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid externalDocumentRef, got %v", err)
+ }
+
+ // TestCase 4: invalid package
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:describesPackage>
+ <spdx:Package rdf:about="http://www.spdx.org/spdxdocs/8f141b09-1138-4fc5-aecb-fc10d9ac1eed#SPDX-1"/>
+ </spdx:describesPackage>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid externalDocumentRef, got %v", err)
+ }
+
+ // TestCase 5: error in extractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:hasExtractedLicensingInfo>
+ <spdx:ExtractedLicensingInfo rdf:about="#LicenseRef-Freeware">
+ <spdx:invalidTag />
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText>
+<![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]>
+ </spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:hasExtractedLicensingInfo>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid extractedLicensingInfo, got %v", err)
+ }
+
+ // TestCase 6: error in annotation
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid extractedLicensingInfo, got %v", err)
+ }
+
+ // TestCase 7: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:unknownTag />
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to unknown predicate, got %v", err)
+ }
+
+ // TestCase 7: everything valid
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:specVersion>SPDX-2.1</spdx:specVersion>
+ <spdx:dataLicense rdf:resource="http://spdx.org/licenses/CC0-1.0" />
+ <spdx:name>/test/example</spdx:name>
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ <spdx:creationInfo>
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: spdx (y)</spdx:creator>
+ <spdx:creator>Organization: </spdx:creator>
+ <spdx:creator>Tool: spdx2</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ </spdx:CreationInfo>
+ </spdx:creationInfo>
+ <rdfs:comment>test</rdfs:comment>
+ <spdx:reviewed>
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ </spdx:reviewed>
+ <spdx:describesPackage>
+ <spdx:Package rdf:about="#SPDXRef-1"/>
+ </spdx:describesPackage>
+ <spdx:hasExtractedLicensingInfo rdf:resource="http://spdx.org/licenses/CC0-1.0"/>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_containedBy"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-1"/>
+ <rdfs:comment></rdfs:comment>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>test annotation</rdfs:comment>
+ <spdx:annotator>Person: Rishabh Bhatnagar</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v3/parser.go b/rdfloader/parser2v3/parser.go
new file mode 100644
index 0000000..961af31
--- /dev/null
+++ b/rdfloader/parser2v3/parser.go
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ gordfWriter "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// returns a new instance of rdfParser2_3 given the gordf object and nodeToTriples mapping
+func NewParser2_3(gordfParserObj *gordfParser.Parser, nodeToTriples map[string][]*gordfParser.Triple) *rdfParser2_3 {
+ parser := rdfParser2_3{
+ gordfParserObj: gordfParserObj,
+ nodeStringToTriples: nodeToTriples,
+ doc: &v2_3.Document{
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{},
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{},
+ Files: []*v2_3.File{},
+ OtherLicenses: []*v2_3.OtherLicense{},
+ Relationships: []*v2_3.Relationship{},
+ Annotations: []*v2_3.Annotation{},
+ Reviews: []*v2_3.Review{},
+ },
+ files: map[common.ElementID]*v2_3.File{},
+ assocWithPackage: map[common.ElementID]bool{},
+ cache: map[string]*nodeState{},
+ }
+ return &parser
+}
+
+// main function which takes in a gordfParser and returns
+// a spdxDocument model or the error encountered while parsing it
+func LoadFromGoRDFParser(gordfParserObj *gordfParser.Parser) (*v2_3.Document, error) {
+ // nodeToTriples is a mapping from a node to list of triples.
+ // for every node in the set of subjects of all the triples,
+ // it provides a list of triples that are associated with that subject node.
+ nodeToTriples := gordfWriter.GetNodeToTriples(gordfParserObj.Triples)
+ parser := NewParser2_3(gordfParserObj, nodeToTriples)
+
+ spdxDocumentNode, err := parser.getSpdxDocNode()
+ if err != nil {
+ return nil, err
+ }
+
+ err = parser.parseSpdxDocumentNode(spdxDocumentNode)
+ if err != nil {
+ return nil, err
+ }
+
+ // parsing other root elements
+ for _, rootNode := range gordfWriter.GetRootNodes(parser.gordfParserObj.Triples) {
+ typeTriples := gordfWriter.FilterTriples(gordfParserObj.Triples, &rootNode.ID, &RDF_TYPE, nil)
+ if len(typeTriples) != 1 {
+ return nil, fmt.Errorf("every node must be associated with exactly 1 type Triple. found %d type triples", len(typeTriples))
+ }
+ switch typeTriples[0].Object.ID {
+ case SPDX_SPDX_DOCUMENT_CAPITALIZED:
+ continue // it is already parsed.
+ case SPDX_SNIPPET:
+ snippet, err := parser.getSnippetInformationFromNode2_3(typeTriples[0].Subject)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing a snippet: %v", err)
+ }
+ err = parser.setSnippetToFileWithID(snippet, snippet.SnippetFromFileSPDXIdentifier)
+ if err != nil {
+ return nil, err
+ }
+ // todo: check other root node attributes.
+ default:
+ continue
+ // because in rdf it is quite possible that the root node is an
+ // element that has been used in the some other element as a child
+ }
+ }
+
+ // parsing packages and files sets the files to a files variable which is
+ // associated with the parser and not the document. following method is
+ // necessary to transfer the files which are not set in the packages to the
+ // Files attribute of the document
+ // WARNING: do not relocate following function call. It must be at the end of the function
+ parser.setUnpackagedFiles()
+ return parser.doc, nil
+}
+
+// from the given parser object, returns the SpdxDocument Node defined in the root elements.
+// returns error if the document is associated with no SpdxDocument or
+// associated with more than one SpdxDocument node.
+func (parser *rdfParser2_3) getSpdxDocNode() (node *gordfParser.Node, err error) {
+ /* Possible Questions:
+ 1. why are you traversing the root nodes only? why not directly filter out
+ all the triples with rdf:type=spdx:SpdxDocument?
+ Ans: It is quite possible that the relatedElement or any other attribute
+ to have dependency of another SpdxDocument. In that case, that
+ element will reference the dependency using SpdxDocument tag which will
+ cause false positives when direct filtering is done.
+ */
+ // iterate over root nodes and find the node which has a property of rdf:type=spdx:SpdxDocument
+ var spdxDocNode *gordfParser.Node
+ for _, rootNode := range gordfWriter.GetRootNodes(parser.gordfParserObj.Triples) {
+ typeTriples := gordfWriter.FilterTriples(
+ parser.nodeToTriples(rootNode), // triples
+ &rootNode.ID, // Subject
+ &RDF_TYPE, // Predicate
+ nil, // Object
+ )
+
+ if typeTriples[0].Object.ID == SPDX_SPDX_DOCUMENT_CAPITALIZED {
+ // we found a SpdxDocument Node
+
+ // must be associated with exactly one rdf:type.
+ if len(typeTriples) != 1 {
+ return nil, fmt.Errorf("rootNode (%v) must be associated with exactly one"+
+ " triple of predicate rdf:type, found %d triples", rootNode, len(typeTriples))
+ }
+
+ // checking if we've already found a node and it is not same as the current one.
+ if spdxDocNode != nil && spdxDocNode.ID != typeTriples[0].Subject.ID {
+ return nil, fmt.Errorf("found more than one SpdxDocument Node (%v and %v)", spdxDocNode, typeTriples[0].Subject)
+ }
+ spdxDocNode = typeTriples[0].Subject
+ }
+ }
+ if spdxDocNode == nil {
+ return nil, errors.New("RDF files must be associated with a SpdxDocument tag. No tag found")
+ }
+ return spdxDocNode, nil
+}
diff --git a/rdfloader/parser2v3/parser_test.go b/rdfloader/parser2v3/parser_test.go
new file mode 100644
index 0000000..5aebe94
--- /dev/null
+++ b/rdfloader/parser2v3/parser_test.go
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+)
+
+func TestNewParser2_3(t *testing.T) {
+ // testing if the attributes are initialised well and no top-level is left uninitialized.
+ // primarily, checking if all the maps are initialized because
+ // uninitialized slices are by default slices of length 0
+ p, _ := parserFromBodyContent(``)
+ parser := NewParser2_3(p.gordfParserObj, p.nodeStringToTriples)
+ if parser.files == nil {
+ t.Errorf("files should've been initialised, got %v", parser.files)
+ }
+ if parser.assocWithPackage == nil {
+ t.Errorf("assocWithPackage should've been initialised, got %v", parser.assocWithPackage)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo should've been initialised, got %v", parser.doc.CreationInfo)
+ }
+ if parser.doc.Packages == nil {
+ t.Errorf("doc.Packages should've been initialised, got %v", parser.doc.Packages)
+ }
+ if parser.doc.Files == nil {
+ t.Errorf("doc.Files should've been initialised, got %v", parser.doc.Files)
+ }
+}
+
+func TestLoadFromGoRDFParser(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+
+ // TestCase 1: gordfparser without a SpdxDocument node triple:
+ parser, _ = parserFromBodyContent("")
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error because of absence of SpdxDocument node, got %v", err)
+ }
+
+ // TestCase 2: invalid SpdxDocumentNode
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301/Document">
+ <spdx:invalidTag />
+ </spdx:SpdxDocument>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error because of absence of SpdxDocument node, got %v", err)
+ }
+
+ // TestCase 3: >1 type triples for subnode of a SpdxDocument:
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#Snippet"/>
+ <spdx:CreationInfo rdf:about="#Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to more than one type triples, got %v", err)
+ }
+
+ // TestCase 4: invalid snippet must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Snippet, got %v", err)
+ }
+
+ // TestCase 5: invalid snippet not associated with any File must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Snippet File, got %v", err)
+ }
+
+ // TestCase 6: other Tag alongwith the SpdxDocument node mustn't raise any error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:review/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ // TestCase 5: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:name>from linux kernel</spdx:name>
+ <spdx:copyrightText>Copyright 2008-2010 John Smith</spdx:copyrightText>
+ <spdx:licenseComments>The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.</spdx:licenseComments>
+ <spdx:snippetFromFile>
+ <spdx:File rdf:about="#SPDXRef-DoapSource">
+ <spdx:copyrightText>Copyright 2010, 2011 Source Auditor Inc.</spdx:copyrightText>
+ <spdx:fileContributor>Open Logic Inc.</spdx:fileContributor>
+ <spdx:fileName>./src/org/spdx/parser/DOAPProject.java</spdx:fileName>
+ <spdx:fileContributor>Black Duck Software In.c</spdx:fileContributor>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/Apache-2.0"/>
+ </spdx:File>
+ </spdx:snippetFromFile>
+ </spdx:Snippet>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err != nil {
+ t.Errorf("error parsing a valid example: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getSpdxDocNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+
+ // TestCase 1: more than one association type for a single node.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Document"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ t.Log(err)
+ if err == nil {
+ t.Errorf("expected and error due to more than one type triples for the SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 2: must be associated with exactly one rdf:type.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Document"/>
+ <spdx:File rdf:about="#SPDXRef-DoapSource"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ t.Log(err)
+ if err == nil {
+ t.Errorf("rootNode must be associated with exactly one triple of predicate rdf:type, got %v", err)
+ }
+
+ // TestCase 3: two different spdx nodes found in a single document.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-1"/>
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-2"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ if err == nil {
+ t.Errorf("expected and error due to more than one type SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 4: no spdx document
+ parser, _ = parserFromBodyContent(``)
+ _, err = parser.getSpdxDocNode()
+ if err == nil {
+ t.Errorf("expected and error due to no SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 5: valid spdxDocument node
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-1"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v3/types.go b/rdfloader/parser2v3/types.go
new file mode 100644
index 0000000..9efd04e
--- /dev/null
+++ b/rdfloader/parser2v3/types.go
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+// copied from tvloader/parser2v3/types.go
+package parser2v3
+
+import (
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+type rdfParser2_3 struct {
+ // fields associated with gordf project which
+ // will be required by rdfloader
+ gordfParserObj *gordfParser.Parser
+ nodeStringToTriples map[string][]*gordfParser.Triple
+
+ // document into which data is being parsed
+ doc *v2_3.Document
+
+ // map of packages and files.
+ files map[common.ElementID]*v2_3.File
+ assocWithPackage map[common.ElementID]bool
+
+ // mapping of nodeStrings to parsed object to save double computation.
+ cache map[string]*nodeState
+}
+
+type Color int
+
+const (
+ GREY Color = iota // represents that the node is being visited
+ WHITE // unvisited node
+ BLACK // visited node
+)
+
+type nodeState struct {
+ // object will be pointer to the parsed or element being parsed.
+ object interface{}
+ // color of a state represents if the node is visited/unvisited/being-visited.
+ Color Color
+}
+
+type AnyLicenseInfo interface {
+ // ToLicenseString returns the representation of license about how it will
+ // be stored in the tools-golang data model
+ ToLicenseString() string
+}
+
+type SimpleLicensingInfo struct {
+ AnyLicenseInfo
+ comment string
+ licenseID string
+ name string
+ seeAlso []string
+ example string
+}
+
+type ExtractedLicensingInfo struct {
+ SimpleLicensingInfo
+ extractedText string
+}
+
+type OrLaterOperator struct {
+ AnyLicenseInfo
+ member SimpleLicensingInfo
+}
+
+type ConjunctiveLicenseSet struct {
+ AnyLicenseInfo
+ members []AnyLicenseInfo
+}
+
+type DisjunctiveLicenseSet struct {
+ AnyLicenseInfo
+ members []AnyLicenseInfo
+}
+
+type License struct {
+ SimpleLicensingInfo
+ isOsiApproved bool
+ licenseText string
+ standardLicenseHeader string
+ standardLicenseTemplate string
+ standardLicenseHeaderTemplate string
+ isDeprecatedLicenseID bool
+ isFsfLibre bool
+}
+
+type ListedLicense struct {
+ License
+}
+
+type LicenseException struct {
+ licenseExceptionId string
+ licenseExceptionText string
+ seeAlso string // must be a valid uri
+ name string
+ example string
+ comment string
+}
+
+type WithExceptionOperator struct {
+ AnyLicenseInfo
+ member SimpleLicensingInfo
+ licenseException LicenseException
+}
+
+// custom LicenseType to provide support for licences of
+// type Noassertion, None and customLicenses
+type SpecialLicense struct {
+ AnyLicenseInfo
+ value SpecialLicenseValue
+}
+
+type SpecialLicenseValue string
+
+const (
+ NONE SpecialLicenseValue = "NONE"
+ NOASSERTION SpecialLicenseValue = "NOASSERTION"
+)
+
+type RangeType string
+
+const (
+ BYTE_RANGE RangeType = "byteRange"
+ LINE_RANGE RangeType = "lineRange"
+)
diff --git a/rdfloader/parser2v3/utils.go b/rdfloader/parser2v3/utils.go
new file mode 100644
index 0000000..356471b
--- /dev/null
+++ b/rdfloader/parser2v3/utils.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ urilib "github.com/spdx/gordf/uri"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// a uri is of type baseURI#fragment or baseFragment/subFragment
+// returns fragment or subFragment when given as an input.
+func getLastPartOfURI(uri string) string {
+ if strings.Contains(uri, "#") {
+ parts := strings.Split(uri, "#")
+ return parts[len(parts)-1]
+ }
+ parts := strings.Split(uri, "/")
+ return parts[len(parts)-1]
+}
+
+func isUriValid(uri string) bool {
+ _, err := urilib.NewURIRef(uri)
+ return err == nil
+}
+
+func getNodeTypeFromTriples(triples []*gordfParser.Triple, node *gordfParser.Node) (string, error) {
+ if node == nil {
+ return "", errors.New("empty node passed to find node type")
+ }
+ typeTriples := rdfwriter.FilterTriples(triples, &node.ID, &RDF_TYPE, nil)
+ switch len(typeTriples) {
+ case 0:
+ return "", fmt.Errorf("node{%v} not associated with any type triple", node)
+ case 1:
+ return typeTriples[0].Object.ID, nil
+ default:
+ return "", fmt.Errorf("node{%v} is associated with more than one type triples", node)
+ }
+}
+
+func (parser *rdfParser2_3) nodeToTriples(node *gordfParser.Node) []*gordfParser.Triple {
+ if node == nil {
+ return []*gordfParser.Triple{}
+ }
+ return parser.nodeStringToTriples[node.String()]
+}
+
+// returns which boolean was given as an input
+// string(bool) is the only possible input for which it will not raise any error.
+func boolFromString(boolString string) (bool, error) {
+ switch strings.ToLower(boolString) {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ default:
+ return false, fmt.Errorf("boolean string can be either true/false")
+ }
+}
+
+/* Function Below this line is taken from the tvloader/parser2v3/utils.go */
+
+// used to extract DocumentRef and SPDXRef values from an SPDX Identifier
+// which can point either to this document or to a different one
+func ExtractDocElementID(value string) (common.DocElementID, error) {
+ docRefID := ""
+ idStr := value
+
+ // check prefix to see if it's a DocumentRef ID
+ if strings.HasPrefix(idStr, "DocumentRef-") {
+ // extract the part that comes between "DocumentRef-" and ":"
+ strs := strings.Split(idStr, ":")
+ // should be exactly two, part before and part after
+ if len(strs) < 2 {
+ return common.DocElementID{}, fmt.Errorf("no colon found although DocumentRef- prefix present")
+ }
+ if len(strs) > 2 {
+ return common.DocElementID{}, fmt.Errorf("more than one colon found")
+ }
+
+ // trim the prefix and confirm non-empty
+ docRefID = strings.TrimPrefix(strs[0], "DocumentRef-")
+ if docRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("document identifier has nothing after prefix")
+ }
+ // and use remainder for element ID parsing
+ idStr = strs[1]
+ }
+
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(idStr, "SPDXRef-") {
+ return common.DocElementID{}, fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(idStr, ":") {
+ // we know this means there was no DocumentRef- prefix, because
+ // we would have handled multiple colons above if it was
+ return common.DocElementID{}, fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(idStr, "SPDXRef-")
+ if eltRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.DocElementID{DocumentRefID: docRefID, ElementRefID: common.ElementID(eltRefID)}, nil
+}
+
+// used to extract SPDXRef values only from an SPDX Identifier which can point
+// to this document only. Use extractDocElementID for parsing IDs that can
+// refer either to this document or a different one.
+func ExtractElementID(value string) (common.ElementID, error) {
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(value, "SPDXRef-") {
+ return common.ElementID(""), fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(value, ":") {
+ return common.ElementID(""), fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(value, "SPDXRef-")
+ if eltRefID == "" {
+ return common.ElementID(""), fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.ElementID(eltRefID), nil
+}
+
+// used to extract key / value from embedded substrings
+// returns subkey, subvalue, nil if no error, or "", "", error otherwise
+func ExtractSubs(value string, sep string) (string, string, error) {
+ // parse the value to see if it's a valid subvalue format
+ sp := strings.SplitN(value, sep, 2)
+ if len(sp) == 1 {
+ return "", "", fmt.Errorf("invalid subvalue format for %s (no %s found)", value, sep)
+ }
+
+ subkey := strings.TrimSpace(sp[0])
+ subvalue := strings.TrimSpace(sp[1])
+
+ return subkey, subvalue, nil
+}
diff --git a/rdfloader/parser2v3/utils_test.go b/rdfloader/parser2v3/utils_test.go
new file mode 100644
index 0000000..24aa4e9
--- /dev/null
+++ b/rdfloader/parser2v3/utils_test.go
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+func Test_getLastPartOfURI(t *testing.T) {
+ // uri of type baseFragment#fragment
+ input := "baseFragment#fragment"
+ expectedOutput := "fragment"
+ output := getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+
+ // uri of type baseFragment/subFragment
+ input = "baseFragment/subFragment"
+ expectedOutput = "subFragment"
+ output = getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+
+ // neither of the case mustn't raise any error.
+ input = "www.github.com"
+ expectedOutput = input
+ output = getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+}
+
+func Test_isUriValid(t *testing.T) {
+ // TestCase 1: Valid Input URI
+ input := "https://www.github.com"
+ isValid := isUriValid(input)
+ if !isValid {
+ t.Errorf("valid input(%s) detected as invalid.", input)
+ }
+
+ // TestCase 2: Invalid Input URI
+ input = `http\:www.github.com`
+ isValid = isUriValid(input)
+ if isValid {
+ t.Errorf("invalid input(%s) detected as valid", input)
+ }
+}
+
+func Test_rdfParser2_3_nodeToTriples(t *testing.T) {
+ var parser *rdfParser2_3
+ var output, expectedOutput []*gordfParser.Triple
+
+ // TestCase 1: a nil node shouldn't raise any error or panic.
+ parser, _ = parserFromBodyContent(``)
+ output = parser.nodeToTriples(nil)
+ if output == nil {
+ t.Errorf("nil input should return an empty slice and not nil")
+ }
+ expectedOutput = []*gordfParser.Triple{}
+ if !reflect.DeepEqual(output, expectedOutput) {
+ t.Errorf("expected %+v, got %+v", expectedOutput, output)
+ }
+
+ // TestCase 2: node should be addressable based on the node content and not the pointer.
+ // It should allow new nodes same as the older ones to retrieve the associated triples.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum rdf:about="#checksum">
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>75068c26abbed3ad3980685bae21d7202d288317</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ newNode := &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#checksum",
+ }
+ output = parser.nodeToTriples(newNode)
+
+ // The output must have 3 triples:
+ // 1. newNode rdf:type Checksum
+ // 2. newNode spdx:algorithm http://spdx.org/rdf/terms#checksumAlgorithm_sha1
+ // 3. newNode spdx:checksumValue 75068c26abbed3ad3980685bae21d7202d288317
+ if len(output) != 3 {
+ t.Errorf("expected output to have 3 triples, got %d", len(output))
+ }
+}
+
+func Test_boolFromString(t *testing.T) {
+ // TestCase 1: Valid Input: "true"
+ // mustn't raise any error
+ input := "true"
+ val, err := boolFromString(input)
+ if err != nil {
+ t.Errorf("function raised an error for a valid input(%s): %s", input, err)
+ }
+ if val != true {
+ t.Errorf("invalid output. Expected %v, found %v", true, val)
+ }
+
+ // TestCase 2: Valid Input: "true"
+ // mustn't raise any error
+ input = "false"
+ val, err = boolFromString(input)
+ if err != nil {
+ t.Errorf("function raised an error for a valid input(%s): %s", input, err)
+ }
+ if val != false {
+ t.Errorf("invalid output. Expected %v, found %v", false, val)
+ }
+
+ // TestCase 3: invalid input: ""
+ // it must raise an error
+ input = ""
+ val, err = boolFromString(input)
+ if err == nil {
+ t.Errorf("invalid input should've raised an error")
+ }
+}
+
+func Test_getNodeTypeFromTriples(t *testing.T) {
+ var err error
+ var node *gordfParser.Node
+ var triples []*gordfParser.Triple
+ var nodeType, expectedNodeType string
+
+ // TestCase 1: nil node must raise an error because,
+ // nil nodes cannot be associated with any rdf:type attribute.
+ _, err = getNodeTypeFromTriples(triples, nil)
+ if err == nil {
+ t.Errorf("expected an error due to nil node, got %v", err)
+ }
+
+ // TestCase 2: none of the triples give information about the rdf:type of a node.
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "N0",
+ }
+ _, err = getNodeTypeFromTriples(triples, node)
+ if err == nil {
+ t.Errorf("expected an error saying no rdf:type found, got %v", err)
+ }
+
+ // TestCase 3: node is associated with exactly one rdf:type triples
+ typeTriple := &gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: RDF_TYPE,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/rdf/terms#Checksum",
+ },
+ }
+ triples = append(triples, typeTriple)
+ expectedNodeType = "http://spdx.org/rdf/terms#Checksum"
+ nodeType, err = getNodeTypeFromTriples(triples, node)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if nodeType != expectedNodeType {
+ t.Errorf("expected: %v, got: %v", nodeType, expectedNodeType)
+ }
+
+ // TestCase 4: node associated with more than one rdf:type triples must raise an error.
+ typeTriple = &gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: RDF_TYPE,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/rdf/terms#Snippet",
+ },
+ }
+ triples = append(triples, typeTriple)
+ _, err = getNodeTypeFromTriples(triples, node)
+ if err == nil {
+ t.Errorf("expected an error saying more than one rdf:type found, got %v", err)
+ }
+}
+
+// following tests are copy pasted from tvloader/parser2v3/util_test.go
+
+func TestCanExtractDocumentAndElementRefsFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractDocElementID(t, "SPDXRef-file1", false, "", "file1")
+ // test with valid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file2", false, "doc2", "file2")
+ // test with invalid ID in this document
+ helperForExtractDocElementID(t, "a:SPDXRef-file1", true, "", "")
+ helperForExtractDocElementID(t, "file1", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-file1:", true, "", "")
+ // test with invalid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:a", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:SPDXRef-file1", true, "", "")
+ // test with invalid formats
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file1:file2", true, "", "")
+}
+
+func helperForExtractDocElementID(t *testing.T, tst string, wantErr bool, wantDoc string, wantElt string) {
+ deID, err := ExtractDocElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if deID.DocumentRefID != wantDoc {
+ if wantDoc == "" {
+ t.Errorf("testing %v: want empty string for DocumentRefID, got %v", tst, deID.DocumentRefID)
+ } else {
+ t.Errorf("testing %v: want %v for DocumentRefID, got %v", tst, wantDoc, deID.DocumentRefID)
+ }
+ }
+ if deID.ElementRefID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, deID.ElementRefID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, deID.ElementRefID)
+ }
+ }
+}
+
+func TestCanExtractElementRefsOnlyFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractElementID(t, "SPDXRef-file1", false, "file1")
+ // test with valid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-file2", true, "")
+ // test with invalid ID in this document
+ helperForExtractElementID(t, "a:SPDXRef-file1", true, "")
+ helperForExtractElementID(t, "file1", true, "")
+ helperForExtractElementID(t, "SPDXRef-", true, "")
+ helperForExtractElementID(t, "SPDXRef-file1:", true, "")
+ // test with invalid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:a", true, "")
+ helperForExtractElementID(t, "DocumentRef-:", true, "")
+ helperForExtractElementID(t, "DocumentRef-:SPDXRef-file1", true, "")
+}
+
+func helperForExtractElementID(t *testing.T, tst string, wantErr bool, wantElt string) {
+ eID, err := ExtractElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if eID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, eID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, eID)
+ }
+ }
+}
+
+func TestCanExtractSubvalues(t *testing.T) {
+ subkey, subvalue, err := ExtractSubs("SHA1: abc123", ":")
+ if err != nil {
+ t.Errorf("got error when calling extractSubs: %v", err)
+ }
+ if subkey != "SHA1" {
+ t.Errorf("got %v for subkey", subkey)
+ }
+ if subvalue != "abc123" {
+ t.Errorf("got %v for subvalue", subvalue)
+ }
+}
+
+func TestReturnsErrorForInvalidSubvalueFormat(t *testing.T) {
+ _, _, err := ExtractSubs("blah", ":")
+ if err == nil {
+ t.Errorf("expected error when calling extractSubs for invalid format (0 colons), got nil")
+ }
+}
diff --git a/rdfloader/rdfloader.go b/rdfloader/rdfloader.go
index 838b0d7..4575799 100644
--- a/rdfloader/rdfloader.go
+++ b/rdfloader/rdfloader.go
@@ -7,7 +7,9 @@ import (
"github.com/spdx/gordf/rdfloader"
"github.com/spdx/tools-golang/rdfloader/parser2v2"
+ "github.com/spdx/tools-golang/rdfloader/parser2v3"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// Takes in a file Reader and returns the pertaining spdx document
@@ -21,3 +23,15 @@ func Load2_2(content io.Reader) (*v2_2.Document, error) {
doc, err := parser2v2.LoadFromGoRDFParser(rdfParserObj)
return doc, err
}
+
+// Takes in a file Reader and returns the pertaining spdx document
+// or the error if any is encountered while setting the doc.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ var rdfParserObj, err = rdfloader.LoadFromReaderObject(content)
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := parser2v3.LoadFromGoRDFParser(rdfParserObj)
+ return doc, err
+}
diff --git a/rdfloader/rdfloader_test.go b/rdfloader/rdfloader_test.go
index 74471c7..4f77bdf 100644
--- a/rdfloader/rdfloader_test.go
+++ b/rdfloader/rdfloader_test.go
@@ -32,3 +32,87 @@ func TestLoad2_2(t *testing.T) {
t.Errorf("expected an error due to no SpdxDocument Node in the document")
}
}
+
+func TestLoad2_3(t *testing.T) {
+ var reader io.Reader
+ var err error
+
+ // TestCase 1: invalid rdf/xml must raise an error
+ reader = strings.NewReader("")
+ _, err = Load2_3(reader)
+ if err == nil {
+ t.Errorf("expected an EOF error reading an empty file, got %v", err)
+ }
+
+ // TestCase 2: Valid rdf/xml but invalid spdx document must raise an error
+ reader = strings.NewReader(`
+ <rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#">
+ </rdf:RDF>
+ `)
+ _, err = Load2_3(reader)
+ if err == nil {
+ t.Errorf("expected an error due to no SpdxDocument Node in the document")
+ }
+
+ // TestCase 3: New SPDX package elements
+ reader = strings.NewReader(`
+ <rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#"
+ xmlns:doap="http://usefulinc.com/ns/doap#"
+ xmlns:j.0="http://www.w3.org/2009/pointers#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DOCUMENT">
+ <spdx:specVersion>SPDX-2.0</spdx:specVersion>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon">
+ <spdx:name>Some-Package</spdx:name>
+ <spdx:primaryPackagePurpose rdf:resource="packagePurpose_container" />
+ <spdx:releaseDate>2021-10-15T02:38:00Z</spdx:releaseDate>
+ <spdx:builtDate>2021-09-15T02:38:00Z</spdx:builtDate>
+ <spdx:validUntilDate>2022-10-15T02:38:00Z</spdx:validUntilDate>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:SpdxDocument>
+ </rdf:RDF>
+ `)
+
+ doc, err := Load2_3(reader)
+ if err != nil {
+ t.Errorf("expected valid SPDX document: %v", err)
+ }
+
+ if doc == nil {
+ t.Fatalf("expected valid SPDX document but got nil")
+ }
+
+ if len(doc.Packages) == 0 {
+ t.Errorf("expected packages but got none")
+ }
+
+ pkg := doc.Packages[0]
+ if pkg.PackageName != "Some-Package" {
+ t.Errorf("expected package nameof Some-Package but got: %s", pkg.PackageName)
+ }
+ if pkg.PrimaryPackagePurpose != "CONTAINER" {
+ t.Errorf("expected package primary purpose of CONTAINER but got: %s", pkg.PrimaryPackagePurpose)
+ }
+ if pkg.ReleaseDate != "2021-10-15T02:38:00Z" {
+ t.Errorf("expected release date of 2021-10-15T02:38:00Z but got: %s", pkg.ReleaseDate)
+ }
+ if pkg.BuiltDate != "2021-09-15T02:38:00Z" {
+ t.Errorf("expected built date of 2021-09-15T02:38:00Z but got: %s", pkg.BuiltDate)
+ }
+ if pkg.ValidUntilDate != "2022-10-15T02:38:00Z" {
+ t.Errorf("expected valid until date of 2022-10-15T02:38:00Z but got: %s", pkg.ValidUntilDate)
+ }
+}
diff --git a/reporter/reporter.go b/reporter/reporter.go
index 3d07668..0a492bc 100644
--- a/reporter/reporter.go
+++ b/reporter/reporter.go
@@ -12,6 +12,7 @@ import (
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 Reporter functions =====
@@ -139,3 +140,66 @@ func countLicenses2_2(pkg *v2_2.Package) (int, int, map[string]int) {
return totalFound, totalNotFound, foundCounts
}
+
+// ===== 2.3 Reporter functions =====
+
+// Generate2_3 takes a Package whose Files have been analyzed and an
+// io.Writer, and outputs to the io.Writer a tabulated count of
+// the number of Files for each unique LicenseConcluded in the set.
+func Generate2_3(pkg *v2_3.Package, w io.Writer) error {
+ if pkg.FilesAnalyzed == false {
+ return fmt.Errorf("Package FilesAnalyzed is false")
+ }
+ totalFound, totalNotFound, foundCounts := countLicenses2_3(pkg)
+
+ wr := tabwriter.NewWriter(w, 0, 0, 2, ' ', tabwriter.AlignRight)
+
+ fmt.Fprintf(wr, "%d\t License found\n", totalFound)
+ fmt.Fprintf(wr, "%d\t License not found\n", totalNotFound)
+ fmt.Fprintf(wr, "%d\t TOTAL\n", totalFound+totalNotFound)
+ fmt.Fprintf(wr, "\n")
+
+ counts := []struct {
+ lic string
+ count int
+ }{}
+ for k, v := range foundCounts {
+ var entry struct {
+ lic string
+ count int
+ }
+ entry.lic = k
+ entry.count = v
+ counts = append(counts, entry)
+ }
+
+ sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
+
+ for _, c := range counts {
+ fmt.Fprintf(wr, "%d\t %s\n", c.count, c.lic)
+ }
+ fmt.Fprintf(wr, "%d\t TOTAL FOUND\n", totalFound)
+
+ wr.Flush()
+ return nil
+}
+
+func countLicenses2_3(pkg *v2_3.Package) (int, int, map[string]int) {
+ if pkg == nil || pkg.Files == nil {
+ return 0, 0, nil
+ }
+
+ totalFound := 0
+ totalNotFound := 0
+ foundCounts := map[string]int{}
+ for _, f := range pkg.Files {
+ if f.LicenseConcluded == "" || f.LicenseConcluded == "NOASSERTION" {
+ totalNotFound++
+ } else {
+ totalFound++
+ foundCounts[f.LicenseConcluded]++
+ }
+ }
+
+ return totalFound, totalNotFound, foundCounts
+}
diff --git a/reporter/reporter_test.go b/reporter/reporter_test.go
index 9795593..7e6fc88 100644
--- a/reporter/reporter_test.go
+++ b/reporter/reporter_test.go
@@ -8,6 +8,7 @@ import (
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 Reporter top-level function tests =====
@@ -257,3 +258,127 @@ func Test2_2NilPackageReturnsZeroCountsOfLicenses(t *testing.T) {
t.Fatalf("expected %v, got %v", 0, len(foundCounts))
}
}
+
+// ===== 2.3 Reporter top-level function tests =====
+func Test2_3ReporterCanMakeReportFromPackage(t *testing.T) {
+ pkg := &v2_3.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_3.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(` 9 License found
+ 3 License not found
+ 12 TOTAL
+
+ 5 GPL-2.0-only
+ 4 MIT
+ 9 TOTAL FOUND
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_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 Test2_3ReporterReturnsErrorIfPackageFilesNotAnalyzed(t *testing.T) {
+ pkg := &v2_3.Package{
+ FilesAnalyzed: false,
+ }
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_3(pkg, &got)
+ if err == nil {
+ t.Errorf("Expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.3 Utility functions =====
+
+func Test2_3CanGetCountsOfLicenses(t *testing.T) {
+ pkg := &v2_3.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_3.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ totalFound, totalNotFound, foundCounts := countLicenses2_3(pkg)
+ if totalFound != 9 {
+ t.Errorf("expected %v, got %v", 9, totalFound)
+ }
+ if totalNotFound != 3 {
+ t.Errorf("expected %v, got %v", 3, totalNotFound)
+ }
+ if len(foundCounts) != 2 {
+ t.Fatalf("expected %v, got %v", 2, len(foundCounts))
+ }
+
+ // foundCounts is a map of license ID to count of licenses
+ // confirm that the results are as expected
+ if foundCounts["GPL-2.0-only"] != 5 {
+ t.Errorf("expected %v, got %v", 5, foundCounts["GPL-2.0-only"])
+ }
+ if foundCounts["MIT"] != 4 {
+ t.Errorf("expected %v, got %v", 4, foundCounts["MIT"])
+ }
+}
+
+func Test2_3NilPackageReturnsZeroCountsOfLicenses(t *testing.T) {
+ totalFound, totalNotFound, foundCounts := countLicenses2_3(nil)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+
+ pkg := &v2_3.Package{}
+ totalFound, totalNotFound, foundCounts = countLicenses2_3(pkg)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+}
diff --git a/spdx/common/checksum.go b/spdx/common/checksum.go
index 02a57ff..aa2ae52 100644
--- a/spdx/common/checksum.go
+++ b/spdx/common/checksum.go
@@ -7,15 +7,23 @@ type ChecksumAlgorithm string
// The checksum algorithms mentioned in the spdxv2.2.0 https://spdx.github.io/spdx-spec/4-file-information/#44-file-checksum
const (
- SHA224 ChecksumAlgorithm = "SHA224"
- SHA1 ChecksumAlgorithm = "SHA1"
- SHA256 ChecksumAlgorithm = "SHA256"
- SHA384 ChecksumAlgorithm = "SHA384"
- SHA512 ChecksumAlgorithm = "SHA512"
- MD2 ChecksumAlgorithm = "MD2"
- MD4 ChecksumAlgorithm = "MD4"
- MD5 ChecksumAlgorithm = "MD5"
- MD6 ChecksumAlgorithm = "MD6"
+ SHA224 ChecksumAlgorithm = "SHA224"
+ SHA1 ChecksumAlgorithm = "SHA1"
+ SHA256 ChecksumAlgorithm = "SHA256"
+ SHA384 ChecksumAlgorithm = "SHA384"
+ SHA512 ChecksumAlgorithm = "SHA512"
+ MD2 ChecksumAlgorithm = "MD2"
+ MD4 ChecksumAlgorithm = "MD4"
+ MD5 ChecksumAlgorithm = "MD5"
+ MD6 ChecksumAlgorithm = "MD6"
+ SHA3_256 ChecksumAlgorithm = "SHA3-256"
+ SHA3_384 ChecksumAlgorithm = "SHA3-384"
+ SHA3_512 ChecksumAlgorithm = "SHA3-512"
+ BLAKE2b_256 ChecksumAlgorithm = "BLAKE2b-256"
+ BLAKE2b_384 ChecksumAlgorithm = "BLAKE2b-384"
+ BLAKE2b_512 ChecksumAlgorithm = "BLAKE2b-512"
+ BLAKE3 ChecksumAlgorithm = "BLAKE3"
+ ADLER32 ChecksumAlgorithm = "ADLER32"
)
// Checksum provides a unique identifier to match analysis information on each specific file in a package.
diff --git a/spdx/common/package.go b/spdx/common/package.go
index e0635df..de5a075 100644
--- a/spdx/common/package.go
+++ b/spdx/common/package.go
@@ -101,5 +101,5 @@ type PackageVerificationCode struct {
// Spec also allows specifying files to exclude from the
// verification code algorithm; intended to enable exclusion of
// the SPDX document file itself.
- ExcludedFiles []string `json:"packageVerificationCodeExcludedFiles"`
+ ExcludedFiles []string `json:"packageVerificationCodeExcludedFiles,omitempty"`
}
diff --git a/spdx/v2_2/annotation.go b/spdx/v2_2/annotation.go
index f2d5bc8..35eddc6 100644
--- a/spdx/v2_2/annotation.go
+++ b/spdx/v2_2/annotation.go
@@ -6,24 +6,24 @@ import "github.com/spdx/tools-golang/spdx/common"
// Annotation is an Annotation section of an SPDX Document for version 2.2 of the spec.
type Annotation struct {
- // 8.1: Annotator
+ // 12.1: Annotator
// Cardinality: conditional (mandatory, one) if there is an Annotation
Annotator common.Annotator `json:"annotator"`
- // 8.2: Annotation Date: YYYY-MM-DDThh:mm:ssZ
+ // 12.2: Annotation Date: YYYY-MM-DDThh:mm:ssZ
// Cardinality: conditional (mandatory, one) if there is an Annotation
AnnotationDate string `json:"annotationDate"`
- // 8.3: Annotation Type: "REVIEW" or "OTHER"
+ // 12.3: Annotation Type: "REVIEW" or "OTHER"
// Cardinality: conditional (mandatory, one) if there is an Annotation
AnnotationType string `json:"annotationType"`
- // 8.4: SPDX Identifier Reference
+ // 12.4: SPDX Identifier Reference
// Cardinality: conditional (mandatory, one) if there is an Annotation
// This field is not used in hierarchical data formats where the referenced element is clear, such as JSON or YAML.
AnnotationSPDXIdentifier common.DocElementID `json:"-"`
- // 8.5: Annotation Comment
+ // 12.5: Annotation Comment
// Cardinality: conditional (mandatory, one) if there is an Annotation
AnnotationComment string `json:"comment"`
}
diff --git a/spdx/v2_2/creation_info.go b/spdx/v2_2/creation_info.go
index b68dade..d56d9a5 100644
--- a/spdx/v2_2/creation_info.go
+++ b/spdx/v2_2/creation_info.go
@@ -7,20 +7,20 @@ import "github.com/spdx/tools-golang/spdx/common"
// CreationInfo is a Document Creation Information section of an
// SPDX Document for version 2.2 of the spec.
type CreationInfo struct {
- // 2.7: License List Version
+ // 6.7: License List Version
// Cardinality: optional, one
LicenseListVersion string `json:"licenseListVersion"`
- // 2.8: Creators: may have multiple keys for Person, Organization
+ // 6.8: Creators: may have multiple keys for Person, Organization
// and/or Tool
// Cardinality: mandatory, one or many
Creators []common.Creator `json:"creators"`
- // 2.9: Created: data format YYYY-MM-DDThh:mm:ssZ
+ // 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ
// Cardinality: mandatory, one
Created string `json:"created"`
- // 2.10: Creator Comment
+ // 6.10: Creator Comment
// Cardinality: optional, one
CreatorComment string `json:"comment"`
}
diff --git a/spdx/v2_2/document.go b/spdx/v2_2/document.go
index d239194..d4d4e50 100644
--- a/spdx/v2_2/document.go
+++ b/spdx/v2_2/document.go
@@ -6,7 +6,7 @@ package v2_2
import "github.com/spdx/tools-golang/spdx/common"
// ExternalDocumentRef is a reference to an external SPDX document
-// as defined in section 2.6 for version 2.2 of the spec.
+// as defined in section 6.6 for version 2.2 of the spec.
type ExternalDocumentRef struct {
// DocumentRefID is the ID string defined in the start of the
// reference. It should _not_ contain the "DocumentRef-" part
@@ -23,32 +23,32 @@ type ExternalDocumentRef struct {
// Document is an SPDX Document for version 2.2 of the spec.
// See https://spdx.github.io/spdx-spec/v2-draft/ (DRAFT)
type Document struct {
- // 2.1: SPDX Version; should be in the format "SPDX-2.2"
+ // 6.1: SPDX Version; should be in the format "SPDX-2.2"
// Cardinality: mandatory, one
SPDXVersion string `json:"spdxVersion"`
- // 2.2: Data License; should be "CC0-1.0"
+ // 6.2: Data License; should be "CC0-1.0"
// Cardinality: mandatory, one
DataLicense string `json:"dataLicense"`
- // 2.3: SPDX Identifier; should be "DOCUMENT" to represent
+ // 6.3: SPDX Identifier; should be "DOCUMENT" to represent
// mandatory identifier of SPDXRef-DOCUMENT
// Cardinality: mandatory, one
SPDXIdentifier common.ElementID `json:"SPDXID"`
- // 2.4: Document Name
+ // 6.4: Document Name
// Cardinality: mandatory, one
DocumentName string `json:"name"`
- // 2.5: Document Namespace
+ // 6.5: Document Namespace
// Cardinality: mandatory, one
DocumentNamespace string `json:"documentNamespace"`
- // 2.6: External Document References
+ // 6.6: External Document References
// Cardinality: optional, one or many
ExternalDocumentReferences []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
- // 2.11: Document Comment
+ // 6.11: Document Comment
// Cardinality: optional, one
DocumentComment string `json:"comment,omitempty"`
diff --git a/spdx/v2_2/file.go b/spdx/v2_2/file.go
index 43c2608..150e79f 100644
--- a/spdx/v2_2/file.go
+++ b/spdx/v2_2/file.go
@@ -6,61 +6,61 @@ import "github.com/spdx/tools-golang/spdx/common"
// File is a File section of an SPDX Document for version 2.2 of the spec.
type File struct {
- // 4.1: File Name
+ // 8.1: File Name
// Cardinality: mandatory, one
FileName string `json:"fileName"`
- // 4.2: File SPDX Identifier: "SPDXRef-[idstring]"
+ // 8.2: File SPDX Identifier: "SPDXRef-[idstring]"
// Cardinality: mandatory, one
FileSPDXIdentifier common.ElementID `json:"SPDXID"`
- // 4.3: File Types
+ // 8.3: File Types
// Cardinality: optional, multiple
FileTypes []string `json:"fileTypes,omitempty"`
- // 4.4: File Checksum: may have keys for SHA1, SHA256 and/or MD5
+ // 8.4: File Checksum: may have keys for SHA1, SHA256 and/or MD5
// Cardinality: mandatory, one SHA1, others may be optionally provided
Checksums []common.Checksum `json:"checksums"`
- // 4.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 8.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
LicenseConcluded string `json:"licenseConcluded"`
- // 4.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 8.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one or many
LicenseInfoInFiles []string `json:"licenseInfoInFiles"`
- // 4.7: Comments on License
+ // 8.7: Comments on License
// Cardinality: optional, one
LicenseComments string `json:"licenseComments,omitempty"`
- // 4.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // 8.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
FileCopyrightText string `json:"copyrightText"`
// DEPRECATED in version 2.1 of spec
- // 4.9-4.11: Artifact of Project variables (defined below)
+ // 8.9-8.11: Artifact of Project variables (defined below)
// Cardinality: optional, one or many
ArtifactOfProjects []*ArtifactOfProject `json:"-"`
- // 4.12: File Comment
+ // 8.12: File Comment
// Cardinality: optional, one
FileComment string `json:"comment,omitempty"`
- // 4.13: File Notice
+ // 8.13: File Notice
// Cardinality: optional, one
FileNotice string `json:"noticeText,omitempty"`
- // 4.14: File Contributor
+ // 8.14: File Contributor
// Cardinality: optional, one or many
FileContributors []string `json:"fileContributors,omitempty"`
- // 4.15: File Attribution Text
+ // 8.15: File Attribution Text
// Cardinality: optional, one or many
FileAttributionTexts []string `json:"attributionTexts,omitempty"`
// DEPRECATED in version 2.0 of spec
- // 4.16: File Dependencies
+ // 8.16: File Dependencies
// Cardinality: optional, one or many
FileDependencies []string `json:"-"`
@@ -74,21 +74,21 @@ type File struct {
}
// ArtifactOfProject is a DEPRECATED collection of data regarding
-// a Package, as defined in sections 4.9-4.11 in version 2.2 of the spec.
+// a Package, as defined in sections 8.9-8.11 in version 2.2 of the spec.
type ArtifactOfProject struct {
// DEPRECATED in version 2.1 of spec
- // 4.9: Artifact of Project Name
+ // 8.9: Artifact of Project Name
// Cardinality: conditional, required if present, one per AOP
Name string
// DEPRECATED in version 2.1 of spec
- // 4.10: Artifact of Project Homepage: URL or "UNKNOWN"
+ // 8.10: Artifact of Project Homepage: URL or "UNKNOWN"
// Cardinality: optional, one per AOP
HomePage string
// DEPRECATED in version 2.1 of spec
- // 4.11: Artifact of Project Uniform Resource Identifier
+ // 8.11: Artifact of Project Uniform Resource Identifier
// Cardinality: optional, one per AOP
URI string
}
diff --git a/spdx/v2_2/other_license.go b/spdx/v2_2/other_license.go
index 1580169..1eaf048 100644
--- a/spdx/v2_2/other_license.go
+++ b/spdx/v2_2/other_license.go
@@ -5,27 +5,27 @@ package v2_2
// OtherLicense is an Other License Information section of an
// SPDX Document for version 2.2 of the spec.
type OtherLicense struct {
- // 6.1: License Identifier: "LicenseRef-[idstring]"
+ // 10.1: License Identifier: "LicenseRef-[idstring]"
// Cardinality: conditional (mandatory, one) if license is not
// on SPDX License List
LicenseIdentifier string `json:"licenseId"`
- // 6.2: Extracted Text
+ // 10.2: Extracted Text
// Cardinality: conditional (mandatory, one) if there is a
// License Identifier assigned
ExtractedText string `json:"extractedText"`
- // 6.3: License Name: single line of text or "NOASSERTION"
+ // 10.3: License Name: single line of text or "NOASSERTION"
// Cardinality: conditional (mandatory, one) if license is not
// on SPDX License List
LicenseName string `json:"name,omitempty"`
- // 6.4: License Cross Reference
+ // 10.4: License Cross Reference
// Cardinality: conditional (optional, one or many) if license
// is not on SPDX License List
LicenseCrossReferences []string `json:"seeAlsos,omitempty"`
- // 6.5: License Comment
+ // 10.5: License Comment
// Cardinality: optional, one
LicenseComment string `json:"comment,omitempty"`
}
diff --git a/spdx/v2_2/package.go b/spdx/v2_2/package.go
index f8eff78..2ca4cb1 100644
--- a/spdx/v2_2/package.go
+++ b/spdx/v2_2/package.go
@@ -11,99 +11,99 @@ type Package struct {
// e.g. included directly in the Document without being in a Package?
IsUnpackaged bool
- // 3.1: Package Name
+ // 7.1: Package Name
// Cardinality: mandatory, one
PackageName string `json:"name"`
- // 3.2: Package SPDX Identifier: "SPDXRef-[idstring]"
+ // 7.2: Package SPDX Identifier: "SPDXRef-[idstring]"
// Cardinality: mandatory, one
PackageSPDXIdentifier common.ElementID `json:"SPDXID"`
- // 3.3: Package Version
+ // 7.3: Package Version
// Cardinality: optional, one
PackageVersion string `json:"versionInfo,omitempty"`
- // 3.4: Package File Name
+ // 7.4: Package File Name
// Cardinality: optional, one
PackageFileName string `json:"packageFileName,omitempty"`
- // 3.5: Package Supplier: may have single result for either Person or Organization,
+ // 7.5: Package Supplier: may have single result for either Person or Organization,
// or NOASSERTION
// Cardinality: optional, one
PackageSupplier *common.Supplier `json:"supplier,omitempty"`
- // 3.6: Package Originator: may have single result for either Person or Organization,
+ // 7.6: Package Originator: may have single result for either Person or Organization,
// or NOASSERTION
// Cardinality: optional, one
PackageOriginator *common.Originator `json:"originator,omitempty"`
- // 3.7: Package Download Location
+ // 7.7: Package Download Location
// Cardinality: mandatory, one
PackageDownloadLocation string `json:"downloadLocation"`
- // 3.8: FilesAnalyzed
+ // 7.8: FilesAnalyzed
// Cardinality: optional, one; default value is "true" if omitted
FilesAnalyzed bool `json:"filesAnalyzed,omitempty"`
// NOT PART OF SPEC: did FilesAnalyzed tag appear?
IsFilesAnalyzedTagPresent bool
- // 3.9: Package Verification Code
+ // 7.9: Package Verification Code
PackageVerificationCode common.PackageVerificationCode `json:"packageVerificationCode"`
- // 3.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
+ // 7.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
// Cardinality: optional, one or many
PackageChecksums []common.Checksum `json:"checksums"`
- // 3.11: Package Home Page
+ // 7.11: Package Home Page
// Cardinality: optional, one
PackageHomePage string `json:"homepage,omitempty"`
- // 3.12: Source Information
+ // 7.12: Source Information
// Cardinality: optional, one
PackageSourceInfo string `json:"sourceInfo,omitempty"`
- // 3.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
PackageLicenseConcluded string `json:"licenseConcluded"`
- // 3.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageLicenseInfoFromFiles []string `json:"licenseInfoFromFiles"`
- // 3.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
PackageLicenseDeclared string `json:"licenseDeclared"`
- // 3.16: Comments on License
+ // 7.16: Comments on License
// Cardinality: optional, one
PackageLicenseComments string `json:"licenseComments,omitempty"`
- // 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
PackageCopyrightText string `json:"copyrightText"`
- // 3.18: Package Summary Description
+ // 7.18: Package Summary Description
// Cardinality: optional, one
PackageSummary string `json:"summary,omitempty"`
- // 3.19: Package Detailed Description
+ // 7.19: Package Detailed Description
// Cardinality: optional, one
PackageDescription string `json:"description,omitempty"`
- // 3.20: Package Comment
+ // 7.20: Package Comment
// Cardinality: optional, one
PackageComment string `json:"comment,omitempty"`
- // 3.21: Package External Reference
+ // 7.21: Package External Reference
// Cardinality: optional, one or many
PackageExternalReferences []*PackageExternalReference `json:"externalRefs,omitempty"`
- // 3.22: Package External Reference Comment
+ // 7.22: Package External Reference Comment
// Cardinality: conditional (optional, one) for each External Reference
// contained within PackageExternalReference2_1 struct, if present
- // 3.23: Package Attribution Text
+ // 7.23: Package Attribution Text
// Cardinality: optional, one or many
PackageAttributionTexts []string `json:"attributionTexts,omitempty"`
@@ -114,7 +114,7 @@ type Package struct {
}
// PackageExternalReference is an External Reference to additional info
-// about a Package, as defined in section 3.21 in version 2.2 of the spec.
+// about a Package, as defined in section 7.21 in version 2.2 of the spec.
type PackageExternalReference struct {
// category is "SECURITY", "PACKAGE-MANAGER" or "OTHER"
Category string `json:"referenceCategory"`
@@ -127,7 +127,7 @@ type PackageExternalReference struct {
// info, metadata or content within the target location
Locator string `json:"referenceLocator"`
- // 3.22: Package External Reference Comment
+ // 7.22: Package External Reference Comment
// Cardinality: conditional (optional, one) for each External Reference
ExternalRefComment string `json:"comment"`
}
diff --git a/spdx/v2_2/relationship.go b/spdx/v2_2/relationship.go
index 6b44898..a93baa7 100644
--- a/spdx/v2_2/relationship.go
+++ b/spdx/v2_2/relationship.go
@@ -8,16 +8,16 @@ import "github.com/spdx/tools-golang/spdx/common"
// version 2.2 of the spec.
type Relationship struct {
- // 7.1: Relationship
+ // 11.1: Relationship
// Cardinality: optional, one or more; one per Relationship
// one mandatory for SPDX Document with multiple packages
// RefA and RefB are first and second item
- // Relationship is type from 7.1.1
+ // Relationship is type from 11.1.1
RefA common.DocElementID `json:"spdxElementId"`
RefB common.DocElementID `json:"relatedSpdxElement"`
Relationship string `json:"relationshipType"`
- // 7.2: Relationship Comment
+ // 11.2: Relationship Comment
// Cardinality: optional, one
RelationshipComment string `json:"comment,omitempty"`
}
diff --git a/spdx/v2_2/review.go b/spdx/v2_2/review.go
index 4cc7c42..22b3b8a 100644
--- a/spdx/v2_2/review.go
+++ b/spdx/v2_2/review.go
@@ -7,19 +7,19 @@ package v2_2
type Review struct {
// DEPRECATED in version 2.0 of spec
- // 9.1: Reviewer
+ // 13.1: Reviewer
// Cardinality: optional, one
Reviewer string
// including AnnotatorType: one of "Person", "Organization" or "Tool"
ReviewerType string
// DEPRECATED in version 2.0 of spec
- // 9.2: Review Date: YYYY-MM-DDThh:mm:ssZ
+ // 13.2: Review Date: YYYY-MM-DDThh:mm:ssZ
// Cardinality: conditional (mandatory, one) if there is a Reviewer
ReviewDate string
// DEPRECATED in version 2.0 of spec
- // 9.3: Review Comment
+ // 13.3: Review Comment
// Cardinality: optional, one
ReviewComment string
}
diff --git a/spdx/v2_2/snippet.go b/spdx/v2_2/snippet.go
index 913007a..61045f1 100644
--- a/spdx/v2_2/snippet.go
+++ b/spdx/v2_2/snippet.go
@@ -7,42 +7,42 @@ import "github.com/spdx/tools-golang/spdx/common"
// Snippet is a Snippet section of an SPDX Document for version 2.2 of the spec.
type Snippet struct {
- // 5.1: Snippet SPDX Identifier: "SPDXRef-[idstring]"
+ // 9.1: Snippet SPDX Identifier: "SPDXRef-[idstring]"
// Cardinality: mandatory, one
SnippetSPDXIdentifier common.ElementID `json:"SPDXID"`
- // 5.2: Snippet from File SPDX Identifier
+ // 9.2: Snippet from File SPDX Identifier
// Cardinality: mandatory, one
SnippetFromFileSPDXIdentifier common.ElementID `json:"snippetFromFile"`
// Ranges denotes the start/end byte offsets or line numbers that the snippet is relevant to
Ranges []common.SnippetRange `json:"ranges"`
- // 5.5: Snippet Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 9.5: Snippet Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
SnippetLicenseConcluded string `json:"licenseConcluded"`
- // 5.6: License Information in Snippet: SPDX License Expression, "NONE" or "NOASSERTION"
+ // 9.6: License Information in Snippet: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: optional, one or many
LicenseInfoInSnippet []string `json:"licenseInfoInSnippets,omitempty"`
- // 5.7: Snippet Comments on License
+ // 9.7: Snippet Comments on License
// Cardinality: optional, one
SnippetLicenseComments string `json:"licenseComments,omitempty"`
- // 5.8: Snippet Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // 9.8: Snippet Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
SnippetCopyrightText string `json:"copyrightText"`
- // 5.9: Snippet Comment
+ // 9.9: Snippet Comment
// Cardinality: optional, one
SnippetComment string `json:"comment,omitempty"`
- // 5.10: Snippet Name
+ // 9.10: Snippet Name
// Cardinality: optional, one
SnippetName string `json:"name,omitempty"`
- // 5.11: Snippet Attribution Text
+ // 9.11: Snippet Attribution Text
// Cardinality: optional, one or many
SnippetAttributionTexts []string `json:"-"`
}
diff --git a/spdx/v2_3/annotation.go b/spdx/v2_3/annotation.go
new file mode 100644
index 0000000..121e995
--- /dev/null
+++ b/spdx/v2_3/annotation.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Annotation is an Annotation section of an SPDX Document for version 2.3 of the spec.
+type Annotation struct {
+ // 12.1: Annotator
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ Annotator common.Annotator `json:"annotator"`
+
+ // 12.2: Annotation Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationDate string `json:"annotationDate"`
+
+ // 12.3: Annotation Type: "REVIEW" or "OTHER"
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationType string `json:"annotationType"`
+
+ // 12.4: SPDX Identifier Reference
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ // This field is not used in hierarchical data formats where the referenced element is clear, such as JSON or YAML.
+ AnnotationSPDXIdentifier common.DocElementID `json:"-" yaml:"-"`
+
+ // 12.5: Annotation Comment
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationComment string `json:"comment"`
+}
diff --git a/spdx/v2_3/creation_info.go b/spdx/v2_3/creation_info.go
new file mode 100644
index 0000000..55fed3d
--- /dev/null
+++ b/spdx/v2_3/creation_info.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// CreationInfo is a Document Creation Information section of an
+// SPDX Document for version 2.3 of the spec.
+type CreationInfo struct {
+ // 6.7: License List Version
+ // Cardinality: optional, one
+ LicenseListVersion string `json:"licenseListVersion"`
+
+ // 6.8: Creators: may have multiple keys for Person, Organization
+ // and/or Tool
+ // Cardinality: mandatory, one or many
+ Creators []common.Creator `json:"creators"`
+
+ // 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: mandatory, one
+ Created string `json:"created"`
+
+ // 6.10: Creator Comment
+ // Cardinality: optional, one
+ CreatorComment string `json:"comment"`
+}
diff --git a/spdx/v2_3/document.go b/spdx/v2_3/document.go
new file mode 100644
index 0000000..32fdb8d
--- /dev/null
+++ b/spdx/v2_3/document.go
@@ -0,0 +1,65 @@
+// Package spdx contains the struct definition for an SPDX Document
+// and its constituent parts.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// ExternalDocumentRef is a reference to an external SPDX document
+// as defined in section 6.6 for version 2.3 of the spec.
+type ExternalDocumentRef struct {
+ // DocumentRefID is the ID string defined in the start of the
+ // reference. It should _not_ contain the "DocumentRef-" part
+ // of the mandatory ID string.
+ DocumentRefID string `json:"externalDocumentId"`
+
+ // URI is the URI defined for the external document
+ URI string `json:"spdxDocument"`
+
+ // Checksum is the actual hash data
+ Checksum common.Checksum `json:"checksum"`
+}
+
+// Document is an SPDX Document for version 2.3 of the spec.
+// See https://spdx.github.io/spdx-spec/v2.3/document-creation-information
+type Document struct {
+ // 6.1: SPDX Version; should be in the format "SPDX-2.3"
+ // Cardinality: mandatory, one
+ SPDXVersion string `json:"spdxVersion"`
+
+ // 6.2: Data License; should be "CC0-1.0"
+ // Cardinality: mandatory, one
+ DataLicense string `json:"dataLicense"`
+
+ // 6.3: SPDX Identifier; should be "DOCUMENT" to represent
+ // mandatory identifier of SPDXRef-DOCUMENT
+ // Cardinality: mandatory, one
+ SPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 6.4: Document Name
+ // Cardinality: mandatory, one
+ DocumentName string `json:"name"`
+
+ // 6.5: Document Namespace
+ // Cardinality: mandatory, one
+ DocumentNamespace string `json:"documentNamespace"`
+
+ // 6.6: External Document References
+ // Cardinality: optional, one or many
+ ExternalDocumentReferences []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
+
+ // 6.11: Document Comment
+ // Cardinality: optional, one
+ DocumentComment string `json:"comment,omitempty"`
+
+ CreationInfo *CreationInfo `json:"creationInfo"`
+ Packages []*Package `json:"packages,omitempty"`
+ Files []*File `json:"files,omitempty"`
+ OtherLicenses []*OtherLicense `json:"hasExtractedLicensingInfos,omitempty"`
+ Relationships []*Relationship `json:"relationships,omitempty"`
+ Annotations []*Annotation `json:"annotations,omitempty"`
+ Snippets []Snippet `json:"snippets,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ Reviews []*Review `json:"-" yaml:"-"`
+}
diff --git a/spdx/v2_3/file.go b/spdx/v2_3/file.go
new file mode 100644
index 0000000..c472fdb
--- /dev/null
+++ b/spdx/v2_3/file.go
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// File is a File section of an SPDX Document for version 2.3 of the spec.
+type File struct {
+ // 8.1: File Name
+ // Cardinality: mandatory, one
+ FileName string `json:"fileName"`
+
+ // 8.2: File SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ FileSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 8.3: File Types
+ // Cardinality: optional, multiple
+ FileTypes []string `json:"fileTypes,omitempty"`
+
+ // 8.4: File Checksum: may have keys for SHA1, SHA256, MD5, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, BLAKE3, ADLER32
+ // Cardinality: mandatory, one SHA1, others may be optionally provided
+ Checksums []common.Checksum `json:"checksums"`
+
+ // 8.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ LicenseConcluded string `json:"licenseConcluded,omitempty"`
+
+ // 8.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many
+ LicenseInfoInFiles []string `json:"licenseInfoInFiles,omitempty"`
+
+ // 8.7: Comments on License
+ // Cardinality: optional, one
+ LicenseComments string `json:"licenseComments,omitempty"`
+
+ // 8.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ FileCopyrightText string `json:"copyrightText"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.9-8.11: Artifact of Project variables (defined below)
+ // Cardinality: optional, one or many
+ ArtifactOfProjects []*ArtifactOfProject `json:"artifactOfs,omitempty"`
+
+ // 8.12: File Comment
+ // Cardinality: optional, one
+ FileComment string `json:"comment,omitempty"`
+
+ // 8.13: File Notice
+ // Cardinality: optional, one
+ FileNotice string `json:"noticeText,omitempty"`
+
+ // 8.14: File Contributor
+ // Cardinality: optional, one or many
+ FileContributors []string `json:"fileContributors,omitempty"`
+
+ // 8.15: File Attribution Text
+ // Cardinality: optional, one or many
+ FileAttributionTexts []string `json:"attributionTexts,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ // 8.16: File Dependencies
+ // Cardinality: optional, one or many
+ FileDependencies []string `json:"fileDependencies,omitempty"`
+
+ // Snippets contained in this File
+ // Note that Snippets could be defined in a different Document! However,
+ // the only ones that _THIS_ document can contain are this ones that are
+ // defined here -- so this should just be an ElementID.
+ Snippets map[common.ElementID]*Snippet `json:"-" yaml:"-"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// ArtifactOfProject is a DEPRECATED collection of data regarding
+// a Package, as defined in sections 8.9-8.11 in version 2.3 of the spec.
+// NOTE: the JSON schema does not define the structure of this object:
+// https://github.com/spdx/spdx-spec/blob/development/v2.3.1/schemas/spdx-schema.json#L480
+type ArtifactOfProject struct {
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.9: Artifact of Project Name
+ // Cardinality: conditional, required if present, one per AOP
+ Name string `json:"name"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.10: Artifact of Project Homepage: URL or "UNKNOWN"
+ // Cardinality: optional, one per AOP
+ HomePage string `json:"homePage"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.11: Artifact of Project Uniform Resource Identifier
+ // Cardinality: optional, one per AOP
+ URI string `json:"URI"`
+}
diff --git a/spdx/v2_3/other_license.go b/spdx/v2_3/other_license.go
new file mode 100644
index 0000000..363bb41
--- /dev/null
+++ b/spdx/v2_3/other_license.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+// OtherLicense is an Other License Information section of an
+// SPDX Document for version 2.3 of the spec.
+type OtherLicense struct {
+ // 10.1: License Identifier: "LicenseRef-[idstring]"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseIdentifier string `json:"licenseId"`
+
+ // 10.2: Extracted Text
+ // Cardinality: conditional (mandatory, one) if there is a
+ // License Identifier assigned
+ ExtractedText string `json:"extractedText"`
+
+ // 10.3: License Name: single line of text or "NOASSERTION"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseName string `json:"name,omitempty"`
+
+ // 10.4: License Cross Reference
+ // Cardinality: conditional (optional, one or many) if license
+ // is not on SPDX License List
+ LicenseCrossReferences []string `json:"seeAlsos,omitempty"`
+
+ // 10.5: License Comment
+ // Cardinality: optional, one
+ LicenseComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_3/package.go b/spdx/v2_3/package.go
new file mode 100644
index 0000000..7df331d
--- /dev/null
+++ b/spdx/v2_3/package.go
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Package is a Package section of an SPDX Document for version 2.3 of the spec.
+type Package struct {
+ // NOT PART OF SPEC
+ // flag: does this "package" contain files that were in fact "unpackaged",
+ // e.g. included directly in the Document without being in a Package?
+ IsUnpackaged bool `json:"-" yaml:"-"`
+
+ // 7.1: Package Name
+ // Cardinality: mandatory, one
+ PackageName string `json:"name"`
+
+ // 7.2: Package SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ PackageSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 7.3: Package Version
+ // Cardinality: optional, one
+ PackageVersion string `json:"versionInfo,omitempty"`
+
+ // 7.4: Package File Name
+ // Cardinality: optional, one
+ PackageFileName string `json:"packageFileName,omitempty"`
+
+ // 7.5: Package Supplier: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageSupplier *common.Supplier `json:"supplier,omitempty"`
+
+ // 7.6: Package Originator: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageOriginator *common.Originator `json:"originator,omitempty"`
+
+ // 7.7: Package Download Location
+ // Cardinality: mandatory, one
+ PackageDownloadLocation string `json:"downloadLocation"`
+
+ // 7.8: FilesAnalyzed
+ // Cardinality: optional, one; default value is "true" if omitted
+ FilesAnalyzed bool `json:"filesAnalyzed,omitempty"`
+ // NOT PART OF SPEC: did FilesAnalyzed tag appear?
+ IsFilesAnalyzedTagPresent bool `json:"-" yaml:"-"`
+
+ // 7.9: Package Verification Code
+ // Cardinality: if FilesAnalyzed == true must be present, if FilesAnalyzed == false must be omitted
+ PackageVerificationCode *common.PackageVerificationCode `json:"packageVerificationCode,omitempty"`
+
+ // 7.10: Package Checksum: may have keys for SHA1, SHA256, MD5, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, BLAKE3, ADLER32
+ // Cardinality: optional, one or many
+ PackageChecksums []common.Checksum `json:"checksums,omitempty"`
+
+ // 7.11: Package Home Page
+ // Cardinality: optional, one
+ PackageHomePage string `json:"homepage,omitempty"`
+
+ // 7.12: Source Information
+ // Cardinality: optional, one
+ PackageSourceInfo string `json:"sourceInfo,omitempty"`
+
+ // 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ PackageLicenseConcluded string `json:"licenseConcluded,omitempty"`
+
+ // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many if filesAnalyzed is true / omitted;
+ // zero (must be omitted) if filesAnalyzed is false
+ PackageLicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"`
+
+ // 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ PackageLicenseDeclared string `json:"licenseDeclared,omitempty"`
+
+ // 7.16: Comments on License
+ // Cardinality: optional, one
+ PackageLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageCopyrightText string `json:"copyrightText"`
+
+ // 7.18: Package Summary Description
+ // Cardinality: optional, one
+ PackageSummary string `json:"summary,omitempty"`
+
+ // 7.19: Package Detailed Description
+ // Cardinality: optional, one
+ PackageDescription string `json:"description,omitempty"`
+
+ // 7.20: Package Comment
+ // Cardinality: optional, one
+ PackageComment string `json:"comment,omitempty"`
+
+ // 7.21: Package External Reference
+ // Cardinality: optional, one or many
+ PackageExternalReferences []*PackageExternalReference `json:"externalRefs,omitempty"`
+
+ // 7.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ // contained within PackageExternalReference2_1 struct, if present
+
+ // 7.23: Package Attribution Text
+ // Cardinality: optional, one or many
+ PackageAttributionTexts []string `json:"attributionTexts,omitempty"`
+
+ // 7.24: Primary Package Purpose
+ // Cardinality: optional, one or many
+ // Allowed values: APPLICATION, FRAMEWORK, LIBRARY, CONTAINER, OPERATING-SYSTEM, DEVICE, FIRMWARE, SOURCE, ARCHIVE, FILE, INSTALL, OTHER
+ PrimaryPackagePurpose string `json:"primaryPackagePurpose,omitempty"`
+
+ // 7.25: Release Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: optional, one
+ ReleaseDate string `json:"releaseDate,omitempty"`
+
+ // 7.26: Build Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: optional, one
+ BuiltDate string `json:"builtDate,omitempty"`
+
+ // 7.27: Valid Until Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: optional, one
+ ValidUntilDate string `json:"validUntilDate,omitempty"`
+
+ // Files contained in this Package
+ Files []*File `json:"files,omitempty"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// PackageExternalReference is an External Reference to additional info
+// about a Package, as defined in section 7.21 in version 2.3 of the spec.
+type PackageExternalReference struct {
+ // category is "SECURITY", "PACKAGE-MANAGER" or "OTHER"
+ Category string `json:"referenceCategory"`
+
+ // type is an [idstring] as defined in Appendix VI;
+ // called RefType here due to "type" being a Golang keyword
+ RefType string `json:"referenceType"`
+
+ // locator is a unique string to access the package-specific
+ // info, metadata or content within the target location
+ Locator string `json:"referenceLocator"`
+
+ // 7.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ ExternalRefComment string `json:"comment"`
+}
diff --git a/spdx/v2_3/relationship.go b/spdx/v2_3/relationship.go
new file mode 100644
index 0000000..af4c07d
--- /dev/null
+++ b/spdx/v2_3/relationship.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Relationship is a Relationship section of an SPDX Document for
+// version 2.3 of the spec.
+type Relationship struct {
+
+ // 11.1: Relationship
+ // Cardinality: optional, one or more; one per Relationship
+ // one mandatory for SPDX Document with multiple packages
+ // RefA and RefB are first and second item
+ // Relationship is type from 11.1.1
+ RefA common.DocElementID `json:"spdxElementId"`
+ RefB common.DocElementID `json:"relatedSpdxElement"`
+ Relationship string `json:"relationshipType"`
+
+ // 11.2: Relationship Comment
+ // Cardinality: optional, one
+ RelationshipComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_3/review.go b/spdx/v2_3/review.go
new file mode 100644
index 0000000..0463807
--- /dev/null
+++ b/spdx/v2_3/review.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+// Review is a Review section of an SPDX Document for version 2.3 of the spec.
+// DEPRECATED in version 2.0 of spec; retained here for compatibility.
+type Review struct {
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.1: Reviewer
+ // Cardinality: optional, one
+ Reviewer string
+ // including AnnotatorType: one of "Person", "Organization" or "Tool"
+ ReviewerType string
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.2: Review Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is a Reviewer
+ ReviewDate string
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.3: Review Comment
+ // Cardinality: optional, one
+ ReviewComment string
+}
diff --git a/spdx/v2_3/snippet.go b/spdx/v2_3/snippet.go
new file mode 100644
index 0000000..d55a1a9
--- /dev/null
+++ b/spdx/v2_3/snippet.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Snippet is a Snippet section of an SPDX Document for version 2.3 of the spec.
+type Snippet struct {
+
+ // 9.1: Snippet SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ SnippetSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 9.2: Snippet from File SPDX Identifier
+ // Cardinality: mandatory, one
+ SnippetFromFileSPDXIdentifier common.ElementID `json:"snippetFromFile"`
+
+ // Ranges denotes the start/end byte offsets or line numbers that the snippet is relevant to
+ Ranges []common.SnippetRange `json:"ranges"`
+
+ // 9.5: Snippet Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ SnippetLicenseConcluded string `json:"licenseConcluded,omitempty"`
+
+ // 9.6: License Information in Snippet: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many
+ LicenseInfoInSnippet []string `json:"licenseInfoInSnippets,omitempty"`
+
+ // 9.7: Snippet Comments on License
+ // Cardinality: optional, one
+ SnippetLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 9.8: Snippet Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ SnippetCopyrightText string `json:"copyrightText"`
+
+ // 9.9: Snippet Comment
+ // Cardinality: optional, one
+ SnippetComment string `json:"comment,omitempty"`
+
+ // 9.10: Snippet Name
+ // Cardinality: optional, one
+ SnippetName string `json:"name,omitempty"`
+
+ // 9.11: Snippet Attribution Text
+ // Cardinality: optional, one or many
+ SnippetAttributionTexts []string `json:"-" yaml:"-"`
+}
diff --git a/spdxlib/described_elements.go b/spdxlib/described_elements.go
index 61833b4..a2a6356 100644
--- a/spdxlib/described_elements.go
+++ b/spdxlib/described_elements.go
@@ -9,6 +9,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// GetDescribedPackageIDs2_1 returns a slice of ElementIDs for all Packages
@@ -112,3 +113,54 @@ func GetDescribedPackageIDs2_2(doc *v2_2.Document) ([]common.ElementID, error) {
return eIDs, nil
}
+
+// GetDescribedPackageIDs2_3 returns a slice of ElementIDs for all Packages
+// in this Document that it "describes," according to SPDX rules:
+// - If the document has only one Package, its ID is returned.
+// - If the document has 2+ Packages, it returns the IDs of those that have
+// a DESCRIBES (or DESCRIBED_BY) relationship to this DOCUMENT.
+func GetDescribedPackageIDs2_3(doc *v2_3.Document) ([]common.ElementID, error) {
+ // if nil Packages map or zero packages in it, return empty slice
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("Packages map is nil")
+ }
+ if len(doc.Packages) == 0 {
+ return nil, fmt.Errorf("no Packages in Document")
+ }
+ if len(doc.Packages) == 1 {
+ // get first (only) one and return its ID
+ for _, pkg := range doc.Packages {
+ return []common.ElementID{pkg.PackageSPDXIdentifier}, nil
+ }
+ }
+
+ // two or more packages, so we need to go through the relationships,
+ // find DESCRIBES or DESCRIBED_BY for this DOCUMENT, verify they are
+ // valid IDs in this document's packages, and return them
+ if doc.Relationships == nil {
+ return nil, fmt.Errorf("multiple Packages in Document but Relationships slice is nil")
+ }
+
+ eIDs, err := FilterRelationships2_3(doc, func(relationship *v2_3.Relationship) *common.ElementID {
+ refDocument := common.MakeDocElementID("", "DOCUMENT")
+
+ if relationship.Relationship == "DESCRIBES" && relationship.RefA == refDocument {
+ return &relationship.RefB.ElementRefID
+ } else if relationship.Relationship == "DESCRIBED_BY" && relationship.RefB == refDocument {
+ return &relationship.RefA.ElementRefID
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if len(eIDs) == 0 {
+ return nil, fmt.Errorf("no DESCRIBES or DESCRIBED_BY relationships found for this Document")
+ }
+
+ eIDs = SortElementIDs(eIDs)
+
+ return eIDs, nil
+}
diff --git a/spdxlib/described_elements_test.go b/spdxlib/described_elements_test.go
index 8ea9cd5..7398ec9 100644
--- a/spdxlib/described_elements_test.go
+++ b/spdxlib/described_elements_test.go
@@ -8,6 +8,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 tests =====
@@ -347,3 +348,172 @@ func Test2_2FailsToGetDescribedPackagesIfNilMap(t *testing.T) {
t.Fatalf("expected non-nil error, got nil")
}
}
+
+// ===== 2.3 tests =====
+
+func Test2_3CanGetIDsOfDescribedPackages(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_3.Relationship{
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_3(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should be three of the five IDs, returned in alphabetical order
+ if len(describedPkgIDs) != 3 {
+ t.Fatalf("expected %d packages, got %d", 3, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+ if describedPkgIDs[1] != common.ElementID("p4") {
+ t.Errorf("expected %v, got %v", common.ElementID("p4"), describedPkgIDs[1])
+ }
+ if describedPkgIDs[2] != common.ElementID("p5") {
+ t.Errorf("expected %v, got %v", common.ElementID("p5"), describedPkgIDs[2])
+ }
+}
+
+func Test2_3GetDescribedPackagesReturnsSinglePackageIfOnlyOne(t *testing.T) {
+ // set up document and one package, but no relationships
+ // b/c only one package
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_3(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should return the single package
+ if len(describedPkgIDs) != 1 {
+ t.Fatalf("expected %d package, got %d", 1, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfMoreThanOneWithoutDescribesRelationship(t *testing.T) {
+ // set up document and multiple packages, but no DESCRIBES relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_3.Relationship{
+ // different relationship
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfMoreThanOneWithNilRelationships(t *testing.T) {
+ // set up document and multiple packages, but no relationships slice
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfZeroPackagesInMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{},
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfNilMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/spdxlib/documents.go b/spdxlib/documents.go
index 8560f08..dc41803 100644
--- a/spdxlib/documents.go
+++ b/spdxlib/documents.go
@@ -7,6 +7,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ValidateDocument2_1 returns an error if the Document is found to be invalid, or nil if the Document is valid.
@@ -68,3 +69,33 @@ func ValidateDocument2_2(doc *v2_2.Document) error {
return nil
}
+
+// ValidateDocument2_3 returns an error if the Document is found to be invalid, or nil if the Document is valid.
+// Currently, this only verifies that all Element IDs mentioned in Relationships exist in the Document as either a
+// Package or an UnpackagedFile.
+func ValidateDocument2_3(doc *v2_3.Document) error {
+ // cache a map of package IDs for quick lookups
+ validElementIDs := make(map[common.ElementID]bool)
+ for _, docPackage := range doc.Packages {
+ validElementIDs[docPackage.PackageSPDXIdentifier] = true
+ }
+
+ for _, unpackagedFile := range doc.Files {
+ validElementIDs[unpackagedFile.FileSPDXIdentifier] = true
+ }
+
+ // add the Document element ID
+ validElementIDs[common.MakeDocElementID("", "DOCUMENT").ElementRefID] = true
+
+ for _, relationship := range doc.Relationships {
+ if !validElementIDs[relationship.RefA.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefA.ElementRefID))
+ }
+
+ if !validElementIDs[relationship.RefB.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefB.ElementRefID))
+ }
+ }
+
+ return nil
+}
diff --git a/spdxlib/documents_test.go b/spdxlib/documents_test.go
index 586cabd..3557d34 100644
--- a/spdxlib/documents_test.go
+++ b/spdxlib/documents_test.go
@@ -8,6 +8,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 tests =====
@@ -181,3 +182,89 @@ func Test2_2InvalidDocumentFailsValidation(t *testing.T) {
t.Fatalf("expected non-nil error, got nil")
}
}
+
+// ===== 2.3 tests =====
+
+func Test2_3ValidDocumentPassesValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ {
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_3(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got: %s", err.Error())
+ }
+}
+
+func Test2_3InvalidDocumentFailsValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // invalid ID p99
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p99"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/spdxlib/relationships.go b/spdxlib/relationships.go
index 5ff4197..aa807d0 100644
--- a/spdxlib/relationships.go
+++ b/spdxlib/relationships.go
@@ -6,6 +6,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// FilterRelationships2_1 returns a slice of Element IDs returned by the given filter closure. The closure is passed
@@ -35,3 +36,17 @@ func FilterRelationships2_2(doc *v2_2.Document, filter func(*v2_2.Relationship)
return elementIDs, nil
}
+
+// FilterRelationships2_3 returns a slice of Element IDs returned by the given filter closure. The closure is passed
+// one relationship at a time, and it can return an ElementID or nil.
+func FilterRelationships2_3(doc *v2_3.Document, filter func(*v2_3.Relationship) *common.ElementID) ([]common.ElementID, error) {
+ elementIDs := []common.ElementID{}
+
+ for _, relationship := range doc.Relationships {
+ if id := filter(relationship); id != nil {
+ elementIDs = append(elementIDs, *id)
+ }
+ }
+
+ return elementIDs, nil
+}
diff --git a/test/v2_3/tv_test.go b/test/v2_3/tv_test.go
new file mode 100644
index 0000000..9be0459
--- /dev/null
+++ b/test/v2_3/tv_test.go
@@ -0,0 +1,491 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/tvloader"
+ "github.com/spdx/tools-golang/tvsaver"
+)
+
+var update = *flag.Bool("update-snapshots", false, "update the example snapshot")
+
+func TestLoad(t *testing.T) {
+ fileName := "../../examples/sample-docs/tv/SPDXTagExample-v2.3.spdx"
+
+ if update {
+ f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ t.Errorf("unable to open file to write SPDX 2.3 example: %v", err)
+ }
+ err = tvsaver.Save2_3(&want, f)
+ if err != nil {
+ t.Errorf("unable to save SPDX 2.3 example: %v", err)
+ }
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := tvloader.Load2_3(file)
+ if err != nil {
+ t.Errorf("Load2_3() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("Got incorrect struct after parsing example")
+ return
+ }
+}
+
+func TestWrite(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want
+ if err := tvsaver.Save2_3(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_3() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original struct we wrote
+ parsedDoc, err := tvloader.Load2_3(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("Got incorrect struct after writing and re-parsing example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example SPDX v2.3 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want = v2_3.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.3",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {CreatorType: "Tool", Creator: "LicenseFind-1.0"},
+ {CreatorType: "Organization", Creator: "ExampleCodeInspect ()"},
+ {CreatorType: "Person", Creator: "Jane Doe ()"},
+ },
+ Created: "2010-01-29T18:30:22Z",
+ CreatorComment: "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ },
+ DocumentComment: "This document was created using SPDX 2.0 using licenses from the web site.",
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{
+ {
+ DocumentRefID: "DocumentRef-spdx-tool-1.2",
+ URI: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ Checksum: common.Checksum{
+ Algorithm: common.SHA1,
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2759",
+ },
+ },
+ },
+ OtherLicenses: []*v2_3.OtherLicense{
+ {
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-4",
+ ExtractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-Beerware-4.2",
+ ExtractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ LicenseComment: "The beerware license has a couple of other standard variants.",
+ LicenseName: "Beer-Ware License (Version 42)",
+ LicenseCrossReferences: []string{"http://people.freebsd.org/~phk/"},
+ },
+ {
+ LicenseIdentifier: "LicenseRef-3",
+ ExtractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ LicenseName: "CyberNeko License",
+ LicenseCrossReferences: []string{
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com",
+ },
+ LicenseComment: "This is tye CyperNeko License",
+ },
+ },
+ Annotations: []*v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Jane Doe ()",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Document level annotation",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Joe Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-02-10T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Suzanne Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-03-13T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "Another example reviewer.",
+ },
+ },
+ Packages: []*v2_3.Package{
+ {
+ PackageName: "glibc",
+ PackageSPDXIdentifier: "SPDXRef-Package",
+ PackageVersion: "2.11.1",
+ PackageFileName: "glibc-2.11.1.tar.gz",
+ PackageSupplier: &common.Supplier{
+ Supplier: "Jane Doe (jane.doe@example.com)",
+ SupplierType: "Person",
+ },
+ PackageOriginator: &common.Originator{
+ Originator: "ExampleCodeInspect (contact@example.com)",
+ OriginatorType: "Organization",
+ },
+ PackageDownloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ FilesAnalyzed: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ ExcludedFiles: []string{"./package.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: "SHA256",
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ },
+ PackageHomePage: "http://ftp.gnu.org/gnu/glibc",
+ PackageSourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ PackageLicenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)",
+ PackageLicenseInfoFromFiles: []string{
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1",
+ },
+ PackageLicenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)",
+ PackageLicenseComments: "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ PackageCopyrightText: "Copyright 2008-2010 John Smith",
+ PackageSummary: "GNU C library.",
+ PackageDescription: "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ PackageComment: "",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "SECURITY",
+ RefType: "cpe23Type",
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ },
+ {
+ Category: "OTHER",
+ RefType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ Locator: "acmecorp/acmenator/4.1.3-alpha",
+ ExternalRefComment: "This is the external ref for Acme",
+ },
+ },
+ PackageAttributionTexts: []string{
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.",
+ },
+ Files: nil,
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "PACKAGE-MANAGER",
+ RefType: "purl",
+ Locator: "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ },
+ },
+ FilesAnalyzed: false,
+ PackageHomePage: "http://www.openjena.org/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageVersion: "3.12.0",
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-Saxon",
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ PackageCopyrightText: "Copyright Saxonica Ltd",
+ PackageDescription: "The Saxon package is a collection of tools for processing XML documents.",
+ PackageDownloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://saxon.sourceforge.net/",
+ PackageLicenseComments: "Other versions available for a commercial license",
+ PackageLicenseConcluded: "MPL-1.0",
+ PackageLicenseDeclared: "MPL-1.0",
+ PackageName: "Saxon",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "8.8",
+ },
+ {
+ PrimaryPackagePurpose: "CONTAINER",
+ PackageSPDXIdentifier: "SPDXRef-CentOS-7",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDescription: "The CentOS container used to run the application.",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "https://www.centos.org/",
+ PackageName: "centos",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "centos7.9.2009",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ },
+ },
+ Files: []*v2_3.File{
+ {
+ FileName: "./src/org/spdx/parser/DOAPProject.java",
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ FileTypes: []string{
+ "SOURCE",
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright 2010, 2011 Source Auditor Inc.",
+ FileContributors: []string{
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c",
+ },
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-CommonsLangSrc",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file is used by Jena",
+ FileCopyrightText: "Copyright 2001-2011 The Apache Software Foundation",
+ FileContributors: []string{"Apache Software Foundation"},
+ FileName: "./lib-source/commons-lang3-3.1-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileNotice: "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-JenaLib",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file belongs to Jena",
+ FileCopyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ FileContributors: []string{"Apache Software Foundation", "Hewlett Packard Inc."},
+ FileName: "./lib-source/jena-2.6.3-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseComments: "This license is used by Jena",
+ LicenseConcluded: "LicenseRef-1",
+ LicenseInfoInFiles: []string{"LicenseRef-1"},
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-File",
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "File Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "File level annotation",
+ },
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ },
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ FileComment: "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ FileCopyrightText: "Copyright 2008-2010 John Smith",
+ FileContributors: []string{"The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"},
+ FileName: "./package/foo.c",
+ FileTypes: []string{"SOURCE"},
+ LicenseComments: "The concluded license was taken from the package level that the file was included in.",
+ LicenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)",
+ LicenseInfoInFiles: []string{"GPL-2.0-only", "LicenseRef-2"},
+ FileNotice: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ },
+ },
+ Snippets: []v2_3.Snippet{
+ {
+ SnippetSPDXIdentifier: "SPDXRef-Snippet",
+ SnippetFromFileSPDXIdentifier: "SPDXRef-DoapSource",
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{
+ Offset: 310,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ Offset: 420,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ {
+ StartPointer: common.SnippetRangePointer{
+ LineNumber: 5,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ LineNumber: 23,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-only",
+ LicenseInfoInSnippet: []string{"GPL-2.0-only"},
+ SnippetLicenseComments: "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ SnippetCopyrightText: "Copyright 2008-2010 John Smith",
+ SnippetComment: "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ SnippetName: "from linux kernel",
+ },
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("spdx-tool-1.2", "ToolsElement"),
+ Relationship: "COPY_OF",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "JenaLib"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "Saxon"),
+ Relationship: "DYNAMIC_LINK",
+ },
+ {
+ RefA: common.MakeDocElementID("", "CommonsLangSrc"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "GENERATED_FROM",
+ },
+ {
+ RefA: common.MakeDocElementID("", "JenaLib"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "File"),
+ RefB: common.MakeDocElementID("", "fromDoap-0"),
+ Relationship: "GENERATED_FROM",
+ },
+ },
+ Reviews: []*v2_3.Review{
+ {
+ Reviewer: "joe@example.com",
+ ReviewerType: "Person",
+ ReviewDate: "2021-11-03T05:43:21Z",
+ ReviewComment: "This is a review comment",
+ },
+ },
+}
diff --git a/tvloader/parser2v3/parse_annotation.go b/tvloader/parser2v3/parse_annotation.go
new file mode 100644
index 0000000..37861ba
--- /dev/null
+++ b/tvloader/parser2v3/parse_annotation.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+)
+
+func (parser *tvParser2_3) parsePairForAnnotation2_3(tag string, value string) error {
+ if parser.ann == nil {
+ return fmt.Errorf("no annotation struct created in parser ann pointer")
+ }
+
+ switch tag {
+ case "Annotator":
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ if subkey == "Person" || subkey == "Organization" || subkey == "Tool" {
+ parser.ann.Annotator.AnnotatorType = subkey
+ parser.ann.Annotator.Annotator = subvalue
+ return nil
+ }
+ return fmt.Errorf("unrecognized Annotator type %v", subkey)
+ case "AnnotationDate":
+ parser.ann.AnnotationDate = value
+ case "AnnotationType":
+ parser.ann.AnnotationType = value
+ case "SPDXREF":
+ deID, err := extractDocElementID(value)
+ if err != nil {
+ return err
+ }
+ parser.ann.AnnotationSPDXIdentifier = deID
+ case "AnnotationComment":
+ parser.ann.AnnotationComment = value
+ default:
+ return fmt.Errorf("received unknown tag %v in Annotation section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_annotation_test.go b/tvloader/parser2v3/parse_annotation_test.go
new file mode 100644
index 0000000..6681ed5
--- /dev/null
+++ b/tvloader/parser2v3/parse_annotation_test.go
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Annotation section tests =====
+func TestParser2_3FailsIfAnnotationNotSet(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePairForAnnotation2_3("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromAnnotation2_3 without setting ann pointer")
+ }
+}
+
+func TestParser2_3FailsIfAnnotationTagUnknown(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_3("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // parse invalid tag, using parsePairForAnnotation2_3(
+ err = parser.parsePairForAnnotation2_3("blah", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfAnnotationFieldsWithoutAnnotation(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("AnnotationDate", "2018-09-15T17:25:00Z")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for AnnotationDate without Annotator first")
+ }
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for AnnotationType without Annotator first")
+ }
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for SPDXREF without Annotator first")
+ }
+ err = parser.parsePair2_3("AnnotationComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for AnnotationComment without Annotator first")
+ }
+}
+
+func TestParser2_3CanParseAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // Annotator without email address
+ err := parser.parsePair2_3("Annotator", "Person: John Doe")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.Annotator.Annotator != "John Doe" {
+ t.Errorf("got %+v for Annotator, expected John Doe", parser.ann.Annotator.Annotator)
+ }
+ if parser.ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf("got %v for AnnotatorType, expected Person", parser.ann.Annotator.AnnotatorType)
+ }
+
+ // Annotation Date
+ dt := "2018-09-15T17:32:00Z"
+ err = parser.parsePair2_3("AnnotationDate", dt)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.AnnotationDate != dt {
+ t.Errorf("got %v for AnnotationDate, expected %v", parser.ann.AnnotationDate, dt)
+ }
+
+ // Annotation type
+ aType := "REVIEW"
+ err = parser.parsePair2_3("AnnotationType", aType)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.AnnotationType != aType {
+ t.Errorf("got %v for AnnotationType, expected %v", parser.ann.AnnotationType, aType)
+ }
+
+ // SPDX Identifier Reference
+ ref := "SPDXRef-30"
+ err = parser.parsePair2_3("SPDXREF", ref)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ deID := parser.ann.AnnotationSPDXIdentifier
+ if deID.DocumentRefID != "" || deID.ElementRefID != "30" {
+ t.Errorf("got %v for SPDXREF, expected %v", parser.ann.AnnotationSPDXIdentifier, "30")
+ }
+
+ // Annotation Comment
+ cmt := "this is a comment"
+ err = parser.parsePair2_3("AnnotationComment", cmt)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.AnnotationComment != cmt {
+ t.Errorf("got %v for AnnotationComment, expected %v", parser.ann.AnnotationComment, cmt)
+ }
+}
+
+func TestParser2_3FailsIfAnnotatorInvalid(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("Annotator", "John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfAnnotatorTypeInvalid(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("Annotator", "Human: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfAnnotationRefInvalid(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_3("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePair2_3("SPDXREF", "blah:other")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v3/parse_creation_info.go b/tvloader/parser2v3/parse_creation_info.go
new file mode 100644
index 0000000..693a56f
--- /dev/null
+++ b/tvloader/parser2v3/parse_creation_info.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromCreationInfo2_3(tag string, value string) error {
+ // fail if not in Creation Info parser state
+ if parser.st != psCreationInfo2_3 {
+ return fmt.Errorf("got invalid state %v in parsePairFromCreationInfo2_3", parser.st)
+ }
+
+ // create an SPDX Creation Info data struct if we don't have one already
+ if parser.doc.CreationInfo == nil {
+ parser.doc.CreationInfo = &v2_3.CreationInfo{}
+ }
+
+ ci := parser.doc.CreationInfo
+ switch tag {
+ case "LicenseListVersion":
+ ci.LicenseListVersion = value
+ case "Creator":
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+
+ creator := common.Creator{Creator: subvalue}
+ switch subkey {
+ case "Person", "Organization", "Tool":
+ creator.CreatorType = subkey
+ default:
+ return fmt.Errorf("unrecognized Creator type %v", subkey)
+ }
+
+ ci.Creators = append(ci.Creators, creator)
+ case "Created":
+ ci.Created = value
+ case "CreatorComment":
+ ci.CreatorComment = value
+
+ // tag for going on to package section
+ case "PackageName":
+ // error if last file does not have an identifier
+ // this may be a null case: can we ever have a "last file" in
+ // the "creation info" state? should go on to "file" state
+ // even when parsing unpackaged files.
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_3 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.st = psPackage2_3
+ parser.pkg = &v2_3.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ return parser.parsePairFromPackage2_3(tag, value)
+ // tag for going on to _unpackaged_ file section
+ case "FileName":
+ // leave pkg as nil, so that packages will be placed in Files
+ parser.st = psFile2_3
+ parser.pkg = nil
+ return parser.parsePairFromFile2_3(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_3
+ return parser.parsePairFromOtherLicense2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in CreationInfo section", tag)
+ }
+
+ return nil
+}
+
+// ===== Helper functions =====
+
+func extractExternalDocumentReference(value string) (string, string, string, string, error) {
+ sp := strings.Split(value, " ")
+ // remove any that are just whitespace
+ keepSp := []string{}
+ for _, s := range sp {
+ ss := strings.TrimSpace(s)
+ if ss != "" {
+ keepSp = append(keepSp, ss)
+ }
+ }
+
+ var documentRefID, uri, alg, checksum string
+
+ // now, should have 4 items (or 3, if Alg and Checksum were joined)
+ // and should be able to map them
+ if len(keepSp) == 4 {
+ documentRefID = keepSp[0]
+ uri = keepSp[1]
+ alg = keepSp[2]
+ // check that colon is present for alg, and remove it
+ if !strings.HasSuffix(alg, ":") {
+ return "", "", "", "", fmt.Errorf("algorithm does not end with colon")
+ }
+ alg = strings.TrimSuffix(alg, ":")
+ checksum = keepSp[3]
+ } else if len(keepSp) == 3 {
+ documentRefID = keepSp[0]
+ uri = keepSp[1]
+ // split on colon into alg and checksum
+ parts := strings.SplitN(keepSp[2], ":", 2)
+ if len(parts) != 2 {
+ return "", "", "", "", fmt.Errorf("missing colon separator between algorithm and checksum")
+ }
+ alg = parts[0]
+ checksum = parts[1]
+ } else {
+ return "", "", "", "", fmt.Errorf("expected 4 elements, got %d", len(keepSp))
+ }
+
+ // additionally, we should be able to parse the first element as a
+ // DocumentRef- ID string, and we should remove that prefix
+ if !strings.HasPrefix(documentRefID, "DocumentRef-") {
+ return "", "", "", "", fmt.Errorf("expected first element to have DocumentRef- prefix")
+ }
+ documentRefID = strings.TrimPrefix(documentRefID, "DocumentRef-")
+ if documentRefID == "" {
+ return "", "", "", "", fmt.Errorf("document identifier has nothing after prefix")
+ }
+
+ return documentRefID, uri, alg, checksum, nil
+}
diff --git a/tvloader/parser2v3/parse_creation_info_test.go b/tvloader/parser2v3/parse_creation_info_test.go
new file mode 100644
index 0000000..24cb80a
--- /dev/null
+++ b/tvloader/parser2v3/parse_creation_info_test.go
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser creation info state change tests =====
+func TestParser2_3CIMovesToPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ pkgName := "testPkg"
+ err := parser.parsePair2_3("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+ // and a package should be created
+ if parser.pkg == nil {
+ t.Fatalf("parser didn't create new package")
+ }
+ // and the package name should be as expected
+ if parser.pkg.PackageName != pkgName {
+ t.Errorf("expected package name %s, got %s", pkgName, parser.pkg.PackageName)
+ }
+ // and the package should default to true for FilesAnalyzed
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected FilesAnalyzed to default to true, got false")
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != false {
+ t.Errorf("expected IsFilesAnalyzedTagPresent to default to false, got true")
+ }
+ // and the package should NOT be in the SPDX Document's map of packages,
+ // because it doesn't have an SPDX identifier yet
+ if len(parser.doc.Packages) != 0 {
+ t.Errorf("expected 0 packages, got %d", len(parser.doc.Packages))
+ }
+}
+
+func TestParser2_3CIMovesToFileAfterParsingFileNameTagWithNoPackages(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+ // and current package should be nil, meaning Files are placed in the
+ // Files map instead of in a Package
+ if parser.pkg != nil {
+ t.Fatalf("expected pkg to be nil, got non-nil pkg")
+ }
+}
+
+func TestParser2_3CIMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+}
+
+func TestParser2_3CIMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+}
+
+func TestParser2_3CIStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+}
+
+func TestParser2_3CIStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this spdx file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+}
+
+func TestParser2_3FailsParsingCreationInfoWithInvalidState(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psPackage2_3,
+ }
+ err := parser.parsePairFromCreationInfo2_3("SPDXVersion", "SPDX-2.3")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+// ===== Creation Info section tests =====
+func TestParser2_3HasCreationInfoAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePairFromCreationInfo2_3("LicenseListVersion", "3.9")
+ if err != nil {
+ t.Errorf("got error when calling parsePairFromCreationInfo2_3: %v", err)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_3CanParseCreationInfoTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // License List Version
+ err := parser.parsePairFromCreationInfo2_3("LicenseListVersion", "2.3")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.LicenseListVersion != "2.3" {
+ t.Errorf("got %v for LicenseListVersion", parser.doc.CreationInfo.LicenseListVersion)
+ }
+
+ // Creators: Persons
+ refPersons := []string{
+ "Person: Person A",
+ "Person: Person B",
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refPersons[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refPersons[1])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.doc.CreationInfo.Creators) != 2 ||
+ parser.doc.CreationInfo.Creators[0].Creator != "Person A" ||
+ parser.doc.CreationInfo.Creators[1].Creator != "Person B" {
+ t.Errorf("got %v for CreatorPersons", parser.doc.CreationInfo.Creators)
+ }
+
+ // Creators: Organizations
+ refOrgs := []string{
+ "Organization: Organization A",
+ "Organization: Organization B",
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refOrgs[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refOrgs[1])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.doc.CreationInfo.Creators) != 4 ||
+ parser.doc.CreationInfo.Creators[2].Creator != "Organization A" ||
+ parser.doc.CreationInfo.Creators[3].Creator != "Organization B" {
+ t.Errorf("got %v for CreatorOrganizations", parser.doc.CreationInfo.Creators)
+ }
+
+ // Creators: Tools
+ refTools := []string{
+ "Tool: Tool A",
+ "Tool: Tool B",
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refTools[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refTools[1])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.doc.CreationInfo.Creators) != 6 ||
+ parser.doc.CreationInfo.Creators[4].Creator != "Tool A" ||
+ parser.doc.CreationInfo.Creators[5].Creator != "Tool B" {
+ t.Errorf("got %v for CreatorTools", parser.doc.CreationInfo.Creators)
+ }
+
+ // Created date
+ err = parser.parsePairFromCreationInfo2_3("Created", "2018-09-10T11:46:00Z")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.Created != "2018-09-10T11:46:00Z" {
+ t.Errorf("got %v for Created", parser.doc.CreationInfo.Created)
+ }
+
+ // Creator Comment
+ err = parser.parsePairFromCreationInfo2_3("CreatorComment", "Blah whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.CreatorComment != "Blah whatever" {
+ t.Errorf("got %v for CreatorComment", parser.doc.CreationInfo.CreatorComment)
+ }
+}
+
+func TestParser2_3InvalidCreatorTagsFail(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePairFromCreationInfo2_3("Creator", "blah: somebody")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+
+ err = parser.parsePairFromCreationInfo2_3("Creator", "Tool with no colons")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+}
+
+func TestParser2_3CreatorTagWithMultipleColonsPasses(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePairFromCreationInfo2_3("Creator", "Tool: tool1:2:3")
+ if err != nil {
+ t.Errorf("unexpected error from parsing valid Creator format")
+ }
+}
+
+func TestParser2_3CIUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePairFromCreationInfo2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_3CICreatesRelationship(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.rln == nil {
+ t.Fatalf("parser didn't create and point to Relationship struct")
+ }
+ if parser.rln != parser.doc.Relationships[0] {
+ t.Errorf("pointer to new Relationship doesn't match idx 0 for doc.Relationships[]")
+ }
+}
+
+func TestParser2_3CICreatesAnnotation(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.ann == nil {
+ t.Fatalf("parser didn't create and point to Annotation struct")
+ }
+ if parser.ann != parser.doc.Annotations[0] {
+ t.Errorf("pointer to new Annotation doesn't match idx 0 for doc.Annotations[]")
+ }
+}
+
+// ===== Helper function tests =====
+
+func TestCanExtractExternalDocumentReference(t *testing.T) {
+ refstring := "DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759"
+ wantDocumentRefID := "spdx-tool-1.2"
+ wantURI := "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+ wantAlg := "SHA1"
+ wantChecksum := "d6a770ba38583ed4bb4525bd96e50461655d2759"
+
+ gotDocumentRefID, gotURI, gotAlg, gotChecksum, err := extractExternalDocumentReference(refstring)
+ if err != nil {
+ t.Errorf("got non-nil error: %v", err)
+ }
+ if wantDocumentRefID != gotDocumentRefID {
+ t.Errorf("wanted document ref ID %s, got %s", wantDocumentRefID, gotDocumentRefID)
+ }
+ if wantURI != gotURI {
+ t.Errorf("wanted URI %s, got %s", wantURI, gotURI)
+ }
+ if wantAlg != gotAlg {
+ t.Errorf("wanted alg %s, got %s", wantAlg, gotAlg)
+ }
+ if wantChecksum != gotChecksum {
+ t.Errorf("wanted checksum %s, got %s", wantChecksum, gotChecksum)
+ }
+}
+
+func TestCanExtractExternalDocumentReferenceWithExtraWhitespace(t *testing.T) {
+ refstring := " DocumentRef-spdx-tool-1.2 \t http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 \t SHA1: \t d6a770ba38583ed4bb4525bd96e50461655d2759"
+ wantDocumentRefID := "spdx-tool-1.2"
+ wantURI := "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+ wantAlg := "SHA1"
+ wantChecksum := "d6a770ba38583ed4bb4525bd96e50461655d2759"
+
+ gotDocumentRefID, gotURI, gotAlg, gotChecksum, err := extractExternalDocumentReference(refstring)
+ if err != nil {
+ t.Errorf("got non-nil error: %v", err)
+ }
+ if wantDocumentRefID != gotDocumentRefID {
+ t.Errorf("wanted document ref ID %s, got %s", wantDocumentRefID, gotDocumentRefID)
+ }
+ if wantURI != gotURI {
+ t.Errorf("wanted URI %s, got %s", wantURI, gotURI)
+ }
+ if wantAlg != gotAlg {
+ t.Errorf("wanted alg %s, got %s", wantAlg, gotAlg)
+ }
+ if wantChecksum != gotChecksum {
+ t.Errorf("wanted checksum %s, got %s", wantChecksum, gotChecksum)
+ }
+}
+
+func TestFailsExternalDocumentReferenceWithInvalidFormats(t *testing.T) {
+ invalidRefs := []string{
+ "whoops",
+ "DocumentRef-",
+ "DocumentRef- ",
+ "DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ "DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 d6a770ba38583ed4bb4525bd96e50461655d2759",
+ "DocumentRef-spdx-tool-1.2",
+ "spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759",
+ }
+ for _, refstring := range invalidRefs {
+ _, _, _, _, err := extractExternalDocumentReference(refstring)
+ if err == nil {
+ t.Errorf("expected non-nil error for %s, got nil", refstring)
+ }
+ }
+}
diff --git a/tvloader/parser2v3/parse_file.go b/tvloader/parser2v3/parse_file.go
new file mode 100644
index 0000000..7f6d99c
--- /dev/null
+++ b/tvloader/parser2v3/parse_file.go
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromFile2_3(tag string, value string) error {
+ // expire fileAOP for anything other than an AOPHomePage or AOPURI
+ // (we'll actually handle the HomePage and URI further below)
+ if tag != "ArtifactOfProjectHomePage" && tag != "ArtifactOfProjectURI" {
+ parser.fileAOP = nil
+ }
+
+ switch tag {
+ // tag for creating new file section
+ case "FileName":
+ // check if the previous file contained an spdx Id or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_3 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.file = &v2_3.File{}
+ parser.file.FileName = value
+ // tag for creating new package section and going back to parsing Package
+ case "PackageName":
+ parser.st = psPackage2_3
+ // check if the previous file contained an spdx Id or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_3 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.file = nil
+ return parser.parsePairFromPackage2_3(tag, value)
+ // tag for going on to snippet section
+ case "SnippetSPDXID":
+ parser.st = psSnippet2_3
+ return parser.parsePairFromSnippet2_3(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_3
+ return parser.parsePairFromOtherLicense2_3(tag, value)
+ // tags for file data
+ case "SPDXID":
+ eID, err := extractElementID(value)
+ if err != nil {
+ return err
+ }
+ parser.file.FileSPDXIdentifier = eID
+ if parser.pkg == nil {
+ if parser.doc.Files == nil {
+ parser.doc.Files = []*v2_3.File{}
+ }
+ parser.doc.Files = append(parser.doc.Files, parser.file)
+ } else {
+ if parser.pkg.Files == nil {
+ parser.pkg.Files = []*v2_3.File{}
+ }
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ }
+ case "FileType":
+ parser.file.FileTypes = append(parser.file.FileTypes, value)
+ case "FileChecksum":
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ if parser.file.Checksums == nil {
+ parser.file.Checksums = []common.Checksum{}
+ }
+ switch common.ChecksumAlgorithm(subkey) {
+ case common.SHA1,
+ common.SHA256,
+ common.MD5,
+ common.SHA3_256,
+ common.SHA3_384,
+ common.SHA3_512,
+ common.BLAKE2b_256,
+ common.BLAKE2b_384,
+ common.BLAKE2b_512,
+ common.BLAKE3,
+ common.ADLER32:
+ algorithm := common.ChecksumAlgorithm(subkey)
+ parser.file.Checksums = append(parser.file.Checksums, common.Checksum{Algorithm: algorithm, Value: subvalue})
+ default:
+ return fmt.Errorf("got unknown checksum type %s", subkey)
+ }
+ case "LicenseConcluded":
+ parser.file.LicenseConcluded = value
+ case "LicenseInfoInFile":
+ parser.file.LicenseInfoInFiles = append(parser.file.LicenseInfoInFiles, value)
+ case "LicenseComments":
+ parser.file.LicenseComments = value
+ case "FileCopyrightText":
+ parser.file.FileCopyrightText = value
+ case "ArtifactOfProjectName":
+ parser.fileAOP = &v2_3.ArtifactOfProject{}
+ parser.file.ArtifactOfProjects = append(parser.file.ArtifactOfProjects, parser.fileAOP)
+ parser.fileAOP.Name = value
+ case "ArtifactOfProjectHomePage":
+ if parser.fileAOP == nil {
+ return fmt.Errorf("no current ArtifactOfProject found")
+ }
+ parser.fileAOP.HomePage = value
+ case "ArtifactOfProjectURI":
+ if parser.fileAOP == nil {
+ return fmt.Errorf("no current ArtifactOfProject found")
+ }
+ parser.fileAOP.URI = value
+ case "FileComment":
+ parser.file.FileComment = value
+ case "FileNotice":
+ parser.file.FileNotice = value
+ case "FileContributor":
+ parser.file.FileContributors = append(parser.file.FileContributors, value)
+ case "FileDependency":
+ parser.file.FileDependencies = append(parser.file.FileDependencies, value)
+ case "FileAttributionText":
+ parser.file.FileAttributionTexts = append(parser.file.FileAttributionTexts, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in File section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_file_test.go b/tvloader/parser2v3/parse_file_test.go
new file mode 100644
index 0000000..c72aa86
--- /dev/null
+++ b/tvloader/parser2v3/parse_file_test.go
@@ -0,0 +1,965 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser file section state change tests =====
+func TestParser2_3FileStartsNewFileAfterParsingFileNameTag(t *testing.T) {
+ // create the first file
+ fileOldName := "f1.txt"
+
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: fileOldName, FileSPDXIdentifier: "f1"},
+ }
+ fileOld := parser.file
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, fileOld)
+ // the Package's Files should have this one only
+ if len(parser.pkg.Files) != 1 {
+ t.Fatalf("expected 1 file, got %d", len(parser.pkg.Files))
+ }
+ if parser.pkg.Files[0] != fileOld {
+ t.Errorf("expected file %v in Files[f1], got %v", fileOld, parser.pkg.Files[0])
+ }
+ if parser.pkg.Files[0].FileName != fileOldName {
+ t.Errorf("expected file name %s in Files[f1], got %s", fileOldName, parser.pkg.Files[0].FileName)
+ }
+
+ // now add a new file
+ fileName := "f2.txt"
+ err := parser.parsePair2_3("FileName", fileName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_3 {
+ t.Errorf("expected state to be %v, got %v", psFile2_3, parser.st)
+ }
+ // and a file should be created
+ if parser.file == nil {
+ t.Fatalf("parser didn't create new file")
+ }
+ // and the file name should be as expected
+ if parser.file.FileName != fileName {
+ t.Errorf("expected file name %s, got %s", fileName, parser.file.FileName)
+ }
+ // and the Package's Files should still be of size 1 and not have this new
+ // one yet, since it hasn't seen an SPDX identifier
+ if len(parser.pkg.Files) != 1 {
+ t.Fatalf("expected 1 file, got %d", len(parser.pkg.Files))
+ }
+ if parser.pkg.Files[0] != fileOld {
+ t.Errorf("expected file %v in Files[f1], got %v", fileOld, parser.pkg.Files[0])
+ }
+ if parser.pkg.Files[0].FileName != fileOldName {
+ t.Errorf("expected file name %s in Files[f1], got %s", fileOldName, parser.pkg.Files[0].FileName)
+ }
+
+ // now parse an SPDX identifier tag
+ err = parser.parsePair2_3("SPDXID", "SPDXRef-f2ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // and the Package's Files should now be of size 2 and have this new one
+ if len(parser.pkg.Files) != 2 {
+ t.Fatalf("expected 2 files, got %d", len(parser.pkg.Files))
+ }
+ if parser.pkg.Files[0] != fileOld {
+ t.Errorf("expected file %v in Files[f1], got %v", fileOld, parser.pkg.Files[0])
+ }
+ if parser.pkg.Files[0].FileName != fileOldName {
+ t.Errorf("expected file name %s in Files[f1], got %s", fileOldName, parser.pkg.Files[0].FileName)
+ }
+ if parser.pkg.Files[1] != parser.file {
+ t.Errorf("expected file %v in Files[f2ID], got %v", parser.file, parser.pkg.Files[1])
+ }
+ if parser.pkg.Files[1].FileName != fileName {
+ t.Errorf("expected file name %s in Files[f2ID], got %s", fileName, parser.pkg.Files[1].FileName)
+ }
+}
+
+func TestParser2_3FileAddsToPackageOrUnpackagedFiles(t *testing.T) {
+ // start with no packages
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // add a file and SPDX identifier
+ fileName := "f2.txt"
+ err := parser.parsePair2_3("FileName", fileName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ err = parser.parsePair2_3("SPDXID", "SPDXRef-f2ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ fileOld := parser.file
+ // should have been added to Files
+ if len(parser.doc.Files) != 1 {
+ t.Fatalf("expected 1 file in Files, got %d", len(parser.doc.Files))
+ }
+ if parser.doc.Files[0] != fileOld {
+ t.Errorf("expected file %v in Files[f2ID], got %v", fileOld, parser.doc.Files[0])
+ }
+ // now create a package and a new file
+ err = parser.parsePair2_3("PackageName", "package1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ err = parser.parsePair2_3("SPDXID", "SPDXRef-pkg1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ err = parser.parsePair2_3("FileName", "f3.txt")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ err = parser.parsePair2_3("SPDXID", "SPDXRef-f3ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // Files should still be size 1 and have old file only
+ if len(parser.doc.Files) != 1 {
+ t.Fatalf("expected 1 file in Files, got %d", len(parser.doc.Files))
+ }
+ if parser.doc.Files[0] != fileOld {
+ t.Errorf("expected file %v in Files[f2ID], got %v", fileOld, parser.doc.Files[0])
+ }
+ // and new package should have gotten the new file
+ if len(parser.pkg.Files) != 1 {
+ t.Fatalf("expected 1 file in Files, got %d", len(parser.pkg.Files))
+ }
+ if parser.pkg.Files[0] != parser.file {
+ t.Errorf("expected file %v in Files[f3ID], got %v", parser.file, parser.pkg.Files[0])
+ }
+}
+
+func TestParser2_3FileStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ // create the first file and package
+ p1Name := "package1"
+ f1Name := "f1.txt"
+
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: p1Name, PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: f1Name, FileSPDXIdentifier: "f1"},
+ }
+ p1 := parser.pkg
+ f1 := parser.file
+ parser.doc.Packages = append(parser.doc.Packages, p1)
+ parser.pkg.Files = append(parser.pkg.Files, f1)
+
+ // now add a new package
+ p2Name := "package2"
+ err := parser.parsePair2_3("PackageName", p2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should go back to Package
+ if parser.st != psPackage2_3 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_3, parser.st)
+ }
+ // and a package should be created
+ if parser.pkg == nil {
+ t.Fatalf("parser didn't create new pkg")
+ }
+ // and the package name should be as expected
+ if parser.pkg.PackageName != p2Name {
+ t.Errorf("expected package name %s, got %s", p2Name, parser.pkg.PackageName)
+ }
+ // and the package should default to true for FilesAnalyzed
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected FilesAnalyzed to default to true, got false")
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != false {
+ t.Errorf("expected IsFilesAnalyzedTagPresent to default to false, got true")
+ }
+ // and the new Package should have no files
+ if len(parser.pkg.Files) != 0 {
+ t.Errorf("Expected no files in pkg.Files, got %d", len(parser.pkg.Files))
+ }
+ // and the Document's Packages should still be of size 1 and not have this
+ // new one, because no SPDX identifier has been seen yet
+ if len(parser.doc.Packages) != 1 {
+ t.Fatalf("expected 1 package, got %d", len(parser.doc.Packages))
+ }
+ if parser.doc.Packages[0] != p1 {
+ t.Errorf("Expected package %v in Packages[package1], got %v", p1, parser.doc.Packages[0])
+ }
+ if parser.doc.Packages[0].PackageName != p1Name {
+ t.Errorf("expected package name %s in Packages[package1], got %s", p1Name, parser.doc.Packages[0].PackageName)
+ }
+ // and the first Package's Files should be of size 1 and have f1 only
+ if len(parser.doc.Packages[0].Files) != 1 {
+ t.Errorf("Expected 1 file in Packages[package1].Files, got %d", len(parser.doc.Packages[0].Files))
+ }
+ if parser.doc.Packages[0].Files[0] != f1 {
+ t.Errorf("Expected file %v in Files[f1], got %v", f1, parser.doc.Packages[0].Files[0])
+ }
+ if parser.doc.Packages[0].Files[0].FileName != f1Name {
+ t.Errorf("expected file name %s in Files[f1], got %s", f1Name, parser.doc.Packages[0].Files[0].FileName)
+ }
+ // and the current file should be nil
+ if parser.file != nil {
+ t.Errorf("Expected nil for parser.file, got %v", parser.file)
+ }
+}
+
+func TestParser2_3FileMovesToSnippetAfterParsingSnippetSPDXIDTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ fileCurrent := parser.file
+
+ err := parser.parsePair2_3("SnippetSPDXID", "SPDXRef-Test1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psSnippet2_3 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_3, parser.st)
+ }
+ // and current file should remain what it was
+ if parser.file != fileCurrent {
+ t.Fatalf("expected file to remain %v, got %v", fileCurrent, parser.file)
+ }
+}
+
+func TestParser2_3FileMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePair2_3("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+}
+
+func TestParser2_3FileMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePair2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+}
+
+func TestParser2_3FileStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psFile2_3 {
+ t.Errorf("expected state to be %v, got %v", psFile2_3, parser.st)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psFile2_3 {
+ t.Errorf("expected state to be %v, got %v", psFile2_3, parser.st)
+ }
+}
+
+func TestParser2_3FileStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+}
+
+// ===== File data section tests =====
+func TestParser2_3CanParseFileTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // File Name
+ err := parser.parsePairFromFile2_3("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.FileName != "f1.txt" {
+ t.Errorf("got %v for FileName", parser.file.FileName)
+ }
+ // should not yet be added to the Packages file list, because we haven't
+ // seen an SPDX identifier yet
+ if len(parser.pkg.Files) != 0 {
+ t.Errorf("expected 0 files, got %d", len(parser.pkg.Files))
+ }
+
+ // File SPDX Identifier
+ err = parser.parsePairFromFile2_3("SPDXID", "SPDXRef-f1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.FileSPDXIdentifier != "f1" {
+ t.Errorf("got %v for FileSPDXIdentifier", parser.file.FileSPDXIdentifier)
+ }
+ // should now be added to the Packages file list
+ if len(parser.pkg.Files) != 1 {
+ t.Errorf("expected 1 file, got %d", len(parser.pkg.Files))
+ }
+ if parser.pkg.Files[0] != parser.file {
+ t.Errorf("expected Files[f1] to be %v, got %v", parser.file, parser.pkg.Files[0])
+ }
+
+ // File Type
+ fileTypes := []string{
+ "TEXT",
+ "DOCUMENTATION",
+ }
+ for _, ty := range fileTypes {
+ err = parser.parsePairFromFile2_3("FileType", ty)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, typeWant := range fileTypes {
+ flagFound := false
+ for _, typeCheck := range parser.file.FileTypes {
+ if typeWant == typeCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in FileTypes", typeWant)
+ }
+ }
+ if len(fileTypes) != len(parser.file.FileTypes) {
+ t.Errorf("expected %d types in FileTypes, got %d", len(fileTypes),
+ len(parser.file.FileTypes))
+ }
+
+ // File Checksums
+ codeSha1 := "85ed0817af83a24ad8da68c2b5094de69833983c"
+ sumSha1 := "SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c"
+ codeSha256 := "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ sumSha256 := "SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ codeMd5 := "624c1abb3664f4b35547e7c73864ad24"
+ sumMd5 := "MD5: 624c1abb3664f4b35547e7c73864ad24"
+ err = parser.parsePairFromFile2_3("FileChecksum", sumSha1)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_3("FileChecksum", sumSha256)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_3("FileChecksum", sumMd5)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ for _, checksum := range parser.file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != codeSha1 {
+ t.Errorf("expected %s for FileChecksumSHA1, got %s", codeSha1, checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != codeSha256 {
+ t.Errorf("expected %s for FileChecksumSHA1, got %s", codeSha256, checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != codeMd5 {
+ t.Errorf("expected %s for FileChecksumSHA1, got %s", codeMd5, checksum.Value)
+ }
+ }
+ }
+ // Concluded License
+ err = parser.parsePairFromFile2_3("LicenseConcluded", "Apache-2.0 OR GPL-2.0-or-later")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.LicenseConcluded != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("got %v for LicenseConcluded", parser.file.LicenseConcluded)
+ }
+
+ // License Information in File
+ lics := []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "CC0-1.0",
+ }
+ for _, lic := range lics {
+ err = parser.parsePairFromFile2_3("LicenseInfoInFile", lic)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, licWant := range lics {
+ flagFound := false
+ for _, licCheck := range parser.file.LicenseInfoInFiles {
+ if licWant == licCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in LicenseInfoInFiles", licWant)
+ }
+ }
+ if len(lics) != len(parser.file.LicenseInfoInFiles) {
+ t.Errorf("expected %d licenses in LicenseInfoInFiles, got %d", len(lics),
+ len(parser.file.LicenseInfoInFiles))
+ }
+
+ // Comments on License
+ err = parser.parsePairFromFile2_3("LicenseComments", "this is a comment")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.LicenseComments != "this is a comment" {
+ t.Errorf("got %v for LicenseComments", parser.file.LicenseComments)
+ }
+
+ // Copyright Text
+ err = parser.parsePairFromFile2_3("FileCopyrightText", "copyright (c) me")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.FileCopyrightText != "copyright (c) me" {
+ t.Errorf("got %v for FileCopyrightText", parser.file.FileCopyrightText)
+ }
+
+ // Artifact of Projects: Name, HomePage and URI
+ // Artifact set 1
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectName", "project1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectHomePage", "http://example.com/1/")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectURI", "http://example.com/1/uri.whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // Artifact set 2 -- just name
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectName", "project2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // Artifact set 3 -- just name and home page
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectName", "project3")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectHomePage", "http://example.com/3/")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // Artifact set 4 -- just name and URI
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectName", "project4")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectURI", "http://example.com/4/uri.whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+
+ if len(parser.file.ArtifactOfProjects) != 4 {
+ t.Fatalf("expected len %d, got %d", 4, len(parser.file.ArtifactOfProjects))
+ }
+
+ aop := parser.file.ArtifactOfProjects[0]
+ if aop.Name != "project1" {
+ t.Errorf("expected %v, got %v", "project1", aop.Name)
+ }
+ if aop.HomePage != "http://example.com/1/" {
+ t.Errorf("expected %v, got %v", "http://example.com/1/", aop.HomePage)
+ }
+ if aop.URI != "http://example.com/1/uri.whatever" {
+ t.Errorf("expected %v, got %v", "http://example.com/1/uri.whatever", aop.URI)
+ }
+
+ aop = parser.file.ArtifactOfProjects[1]
+ if aop.Name != "project2" {
+ t.Errorf("expected %v, got %v", "project2", aop.Name)
+ }
+ if aop.HomePage != "" {
+ t.Errorf("expected %v, got %v", "", aop.HomePage)
+ }
+ if aop.URI != "" {
+ t.Errorf("expected %v, got %v", "", aop.URI)
+ }
+
+ aop = parser.file.ArtifactOfProjects[2]
+ if aop.Name != "project3" {
+ t.Errorf("expected %v, got %v", "project3", aop.Name)
+ }
+ if aop.HomePage != "http://example.com/3/" {
+ t.Errorf("expected %v, got %v", "http://example.com/3/", aop.HomePage)
+ }
+ if aop.URI != "" {
+ t.Errorf("expected %v, got %v", "", aop.URI)
+ }
+
+ aop = parser.file.ArtifactOfProjects[3]
+ if aop.Name != "project4" {
+ t.Errorf("expected %v, got %v", "project4", aop.Name)
+ }
+ if aop.HomePage != "" {
+ t.Errorf("expected %v, got %v", "", aop.HomePage)
+ }
+ if aop.URI != "http://example.com/4/uri.whatever" {
+ t.Errorf("expected %v, got %v", "http://example.com/4/uri.whatever", aop.URI)
+ }
+
+ // File Comment
+ err = parser.parsePairFromFile2_3("FileComment", "this is a comment")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.FileComment != "this is a comment" {
+ t.Errorf("got %v for FileComment", parser.file.FileComment)
+ }
+
+ // File Notice
+ err = parser.parsePairFromFile2_3("FileNotice", "this is a Notice")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.file.FileNotice != "this is a Notice" {
+ t.Errorf("got %v for FileNotice", parser.file.FileNotice)
+ }
+
+ // File Contributor
+ contribs := []string{
+ "John Doe jdoe@example.com",
+ "EvilCorp",
+ }
+ for _, contrib := range contribs {
+ err = parser.parsePairFromFile2_3("FileContributor", contrib)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, contribWant := range contribs {
+ flagFound := false
+ for _, contribCheck := range parser.file.FileContributors {
+ if contribWant == contribCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in FileContributors", contribWant)
+ }
+ }
+ if len(contribs) != len(parser.file.FileContributors) {
+ t.Errorf("expected %d contribenses in FileContributors, got %d", len(contribs),
+ len(parser.file.FileContributors))
+ }
+
+ // File Dependencies
+ deps := []string{
+ "f-1.txt",
+ "g.txt",
+ }
+ for _, dep := range deps {
+ err = parser.parsePairFromFile2_3("FileDependency", dep)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, depWant := range deps {
+ flagFound := false
+ for _, depCheck := range parser.file.FileDependencies {
+ if depWant == depCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in FileDependency", depWant)
+ }
+ }
+ if len(deps) != len(parser.file.FileDependencies) {
+ t.Errorf("expected %d depenses in FileDependency, got %d", len(deps),
+ len(parser.file.FileDependencies))
+ }
+
+ // File Attribution Texts
+ attrs := []string{
+ "Include this notice in all advertising materials",
+ "This is a \nmulti-line string",
+ }
+ for _, attr := range attrs {
+ err = parser.parsePairFromFile2_3("FileAttributionText", attr)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, attrWant := range attrs {
+ flagFound := false
+ for _, attrCheck := range parser.file.FileAttributionTexts {
+ if attrWant == attrCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in FileAttributionText", attrWant)
+ }
+ }
+ if len(attrs) != len(parser.file.FileAttributionTexts) {
+ t.Errorf("expected %d attribution texts in FileAttributionTexts, got %d", len(attrs),
+ len(parser.file.FileAttributionTexts))
+ }
+
+}
+
+func TestParser2_3FileCreatesRelationshipInDocument(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.rln == nil {
+ t.Fatalf("parser didn't create and point to Relationship struct")
+ }
+ if parser.rln != parser.doc.Relationships[0] {
+ t.Errorf("pointer to new Relationship doesn't match idx 0 for doc.Relationships[]")
+ }
+}
+
+func TestParser2_3FileCreatesAnnotationInDocument(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.ann == nil {
+ t.Fatalf("parser didn't create and point to Annotation struct")
+ }
+ if parser.ann != parser.doc.Annotations[0] {
+ t.Errorf("pointer to new Annotation doesn't match idx 0 for doc.Annotations[]")
+ }
+}
+
+func TestParser2_3FileUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePairFromFile2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestFileAOPPointerChangesAfterTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePairFromFile2_3("ArtifactOfProjectName", "project1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.fileAOP == nil {
+ t.Errorf("expected non-nil AOP pointer, got nil")
+ }
+ curPtr := parser.fileAOP
+
+ // now, a home page; pointer should stay
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectHomePage", "http://example.com/1/")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.fileAOP != curPtr {
+ t.Errorf("expected no change in AOP pointer, was %v, got %v", curPtr, parser.fileAOP)
+ }
+
+ // a URI; pointer should stay
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectURI", "http://example.com/1/uri.whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.fileAOP != curPtr {
+ t.Errorf("expected no change in AOP pointer, was %v, got %v", curPtr, parser.fileAOP)
+ }
+
+ // now, another artifact name; pointer should change but be non-nil
+ // now, a home page; pointer should stay
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectName", "project2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.fileAOP == curPtr {
+ t.Errorf("expected change in AOP pointer, got no change")
+ }
+
+ // finally, an unrelated tag; pointer should go away
+ err = parser.parsePairFromFile2_3("FileComment", "whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.fileAOP != nil {
+ t.Errorf("expected nil AOP pointer, got %v", parser.fileAOP)
+ }
+}
+
+func TestParser2_3FailsIfInvalidSPDXIDInFileSection(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_3("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid SPDX Identifier
+ err = parser.parsePairFromFile2_3("SPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfInvalidChecksumFormatInFileSection(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_3("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid format for checksum line, missing colon
+ err = parser.parsePairFromFile2_3("FileChecksum", "SHA1 85ed0817af83a24ad8da68c2b5094de69833983c")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfUnknownChecksumTypeInFileSection(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_3("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // unknown checksum type
+ err = parser.parsePairFromFile2_3("FileChecksum", "Special: 85ed0817af83a24ad8da68c2b5094de69833983c")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfArtifactHomePageBeforeArtifactName(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_3("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // artifact home page appears before artifact name
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectHomePage", "https://example.com")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfArtifactURIBeforeArtifactName(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_3("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // artifact home page appears before artifact name
+ err = parser.parsePairFromFile2_3("ArtifactOfProjectURI", "https://example.com")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FilesWithoutSpdxIdThrowError(t *testing.T) {
+ // case 1: The previous file (packaged or unpackaged) does not contain spdx ID
+ parser1 := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ file: &v2_3.File{FileName: "FileName"},
+ }
+
+ err := parser1.parsePair2_3("FileName", "f2")
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+
+ // case 2: Invalid file with snippet
+ // Last unpackaged file before the snippet start
+ fileName := "f2.txt"
+ sid1 := common.ElementID("s1")
+ parser2 := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ file: &v2_3.File{FileName: fileName},
+ }
+ err = parser2.parsePair2_3("SnippetSPDXID", string(sid1))
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+
+ // case 3: Invalid File without snippets
+ // Last unpackaged file before the package starts
+ // Last file of a package and New package starts
+ parser3 := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ fileName = "f3.txt"
+ err = parser3.parsePair2_3("FileName", fileName)
+ if err != nil {
+ t.Errorf("%s", err)
+ }
+ err = parser3.parsePair2_3("PackageName", "p2")
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v3/parse_other_license.go b/tvloader/parser2v3/parse_other_license.go
new file mode 100644
index 0000000..d2f41ea
--- /dev/null
+++ b/tvloader/parser2v3/parse_other_license.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromOtherLicense2_3(tag string, value string) error {
+ switch tag {
+ // tag for creating new other license section
+ case "LicenseID":
+ parser.otherLic = &v2_3.OtherLicense{}
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.otherLic.LicenseIdentifier = value
+ case "ExtractedText":
+ parser.otherLic.ExtractedText = value
+ case "LicenseName":
+ parser.otherLic.LicenseName = value
+ case "LicenseCrossReference":
+ parser.otherLic.LicenseCrossReferences = append(parser.otherLic.LicenseCrossReferences, value)
+ case "LicenseComment":
+ parser.otherLic.LicenseComment = value
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in OtherLicense section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_other_license_test.go b/tvloader/parser2v3/parse_other_license_test.go
new file mode 100644
index 0000000..2939e67
--- /dev/null
+++ b/tvloader/parser2v3/parse_other_license_test.go
@@ -0,0 +1,339 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser other license section state change tests =====
+func TestParser2_3OLStartsNewOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ // create the first other license
+ olid1 := "LicenseRef-Lic11"
+ olname1 := "License 11"
+
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: olid1,
+ LicenseName: olname1,
+ },
+ }
+ olic1 := parser.otherLic
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ // the Document's OtherLicenses should have this one only
+ if parser.doc.OtherLicenses[0] != olic1 {
+ t.Errorf("Expected other license %v in OtherLicenses[0], got %v", olic1, parser.doc.OtherLicenses[0])
+ }
+ if parser.doc.OtherLicenses[0].LicenseName != olname1 {
+ t.Errorf("expected other license name %s in OtherLicenses[0], got %s", olname1, parser.doc.OtherLicenses[0].LicenseName)
+ }
+
+ // now add a new other license
+ olid2 := "LicenseRef-22"
+ olname2 := "License 22"
+ err := parser.parsePair2_3("LicenseID", olid2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+ // and an other license should be created
+ if parser.otherLic == nil {
+ t.Fatalf("parser didn't create new other license")
+ }
+ // also parse the new license's name
+ err = parser.parsePair2_3("LicenseName", olname2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should still be correct
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+ // and the other license name should be as expected
+ if parser.otherLic.LicenseName != olname2 {
+ t.Errorf("expected other license name %s, got %s", olname2, parser.otherLic.LicenseName)
+ }
+ // and the Document's Other Licenses should be of size 2 and have these two
+ if len(parser.doc.OtherLicenses) != 2 {
+ t.Fatalf("Expected OtherLicenses to have len 2, got %d", len(parser.doc.OtherLicenses))
+ }
+ if parser.doc.OtherLicenses[0] != olic1 {
+ t.Errorf("Expected other license %v in OtherLicenses[0], got %v", olic1, parser.doc.OtherLicenses[0])
+ }
+ if parser.doc.OtherLicenses[0].LicenseIdentifier != olid1 {
+ t.Errorf("expected other license ID %s in OtherLicenses[0], got %s", olid1, parser.doc.OtherLicenses[0].LicenseIdentifier)
+ }
+ if parser.doc.OtherLicenses[0].LicenseName != olname1 {
+ t.Errorf("expected other license name %s in OtherLicenses[0], got %s", olname1, parser.doc.OtherLicenses[0].LicenseName)
+ }
+ if parser.doc.OtherLicenses[1] != parser.otherLic {
+ t.Errorf("Expected other license %v in OtherLicenses[1], got %v", parser.otherLic, parser.doc.OtherLicenses[1])
+ }
+ if parser.doc.OtherLicenses[1].LicenseIdentifier != olid2 {
+ t.Errorf("expected other license ID %s in OtherLicenses[1], got %s", olid2, parser.doc.OtherLicenses[1].LicenseIdentifier)
+ }
+ if parser.doc.OtherLicenses[1].LicenseName != olname2 {
+ t.Errorf("expected other license name %s in OtherLicenses[1], got %s", olname2, parser.doc.OtherLicenses[1].LicenseName)
+ }
+}
+
+func TestParser2_3OLMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ err := parser.parsePair2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+}
+
+func TestParser2_3OtherLicenseStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-whatever",
+ LicenseName: "the whatever license",
+ },
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+ // and the relationship should be in the Document's Relationships
+ if len(parser.doc.Relationships) != 1 {
+ t.Fatalf("expected doc.Relationships to have len 1, got %d", len(parser.doc.Relationships))
+ }
+ deID := parser.doc.Relationships[0].RefA
+ if deID.DocumentRefID != "" || deID.ElementRefID != "blah" {
+ t.Errorf("expected RefA to be %s, got %s", "blah", parser.doc.Relationships[0].RefA)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+}
+
+func TestParser2_3OtherLicenseStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-whatever",
+ LicenseName: "the whatever license",
+ },
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+
+ // and the annotation should be in the Document's Annotations
+ if len(parser.doc.Annotations) != 1 {
+ t.Fatalf("expected doc.Annotations to have len 1, got %d", len(parser.doc.Annotations))
+ }
+ if parser.doc.Annotations[0].Annotator.Annotator != "John Doe ()" {
+ t.Errorf("expected Annotator to be %s, got %s", "John Doe ()", parser.doc.Annotations[0].Annotator)
+ }
+}
+
+func TestParser2_3OLFailsAfterParsingOtherSectionTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ // can't go back to old sections
+ err := parser.parsePair2_3("SPDXVersion", "SPDX-2.3")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+ err = parser.parsePair2_3("PackageName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+ err = parser.parsePair2_3("FileName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+}
+
+// ===== Other License data section tests =====
+func TestParser2_3CanParseOtherLicenseTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ // License Identifier
+ err := parser.parsePairFromOtherLicense2_3("LicenseID", "LicenseRef-Lic11")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.otherLic.LicenseIdentifier != "LicenseRef-Lic11" {
+ t.Errorf("got %v for LicenseID", parser.otherLic.LicenseIdentifier)
+ }
+
+ // Extracted Text
+ err = parser.parsePairFromOtherLicense2_3("ExtractedText", "You are permitted to do anything with the software, hooray!")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.otherLic.ExtractedText != "You are permitted to do anything with the software, hooray!" {
+ t.Errorf("got %v for ExtractedText", parser.otherLic.ExtractedText)
+ }
+
+ // License Name
+ err = parser.parsePairFromOtherLicense2_3("LicenseName", "License 11")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.otherLic.LicenseName != "License 11" {
+ t.Errorf("got %v for LicenseName", parser.otherLic.LicenseName)
+ }
+
+ // License Cross Reference
+ crossRefs := []string{
+ "https://example.com/1",
+ "https://example.com/2",
+ "https://example.com/3",
+ }
+ for _, cr := range crossRefs {
+ err = parser.parsePairFromOtherLicense2_3("LicenseCrossReference", cr)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, refWant := range crossRefs {
+ flagFound := false
+ for _, refCheck := range parser.otherLic.LicenseCrossReferences {
+ if refWant == refCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in LicenseCrossReferences", refWant)
+ }
+ }
+ if len(crossRefs) != len(parser.otherLic.LicenseCrossReferences) {
+ t.Errorf("expected %d types in LicenseCrossReferences, got %d", len(crossRefs),
+ len(parser.otherLic.LicenseCrossReferences))
+ }
+
+ // License Comment
+ err = parser.parsePairFromOtherLicense2_3("LicenseComment", "this is a comment")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.otherLic.LicenseComment != "this is a comment" {
+ t.Errorf("got %v for LicenseComment", parser.otherLic.LicenseComment)
+ }
+}
+
+func TestParser2_3OLUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psOtherLicense2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+
+ err := parser.parsePairFromOtherLicense2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
diff --git a/tvloader/parser2v3/parse_package.go b/tvloader/parser2v3/parse_package.go
new file mode 100644
index 0000000..d7c87e1
--- /dev/null
+++ b/tvloader/parser2v3/parse_package.go
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromPackage2_3(tag string, value string) error {
+ // expire pkgExtRef for anything other than a comment
+ // (we'll actually handle the comment further below)
+ if tag != "ExternalRefComment" {
+ parser.pkgExtRef = nil
+ }
+
+ switch tag {
+ case "PackageName":
+ // if package already has a name, create and go on to a new package
+ if parser.pkg == nil || parser.pkg.PackageName != "" {
+ // check if the previous package contained an spdx Id or not
+ if parser.pkg != nil && parser.pkg.PackageSPDXIdentifier == nullSpdxElementId2_3 {
+ return fmt.Errorf("package with PackageName %s does not have SPDX identifier", parser.pkg.PackageName)
+ }
+ parser.pkg = &v2_3.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ }
+ parser.pkg.PackageName = value
+ // tag for going on to file section
+ case "FileName":
+ parser.st = psFile2_3
+ return parser.parsePairFromFile2_3(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_3
+ return parser.parsePairFromOtherLicense2_3(tag, value)
+ case "SPDXID":
+ eID, err := extractElementID(value)
+ if err != nil {
+ return err
+ }
+ parser.pkg.PackageSPDXIdentifier = eID
+ if parser.doc.Packages == nil {
+ parser.doc.Packages = []*v2_3.Package{}
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ case "PackageVersion":
+ parser.pkg.PackageVersion = value
+ case "PackageFileName":
+ parser.pkg.PackageFileName = value
+ case "PackageSupplier":
+ supplier := &common.Supplier{Supplier: value}
+ if value == "NOASSERTION" {
+ parser.pkg.PackageSupplier = supplier
+ break
+ }
+
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ switch subkey {
+ case "Person", "Organization":
+ supplier.Supplier = subvalue
+ supplier.SupplierType = subkey
+ default:
+ return fmt.Errorf("unrecognized PackageSupplier type %v", subkey)
+ }
+ parser.pkg.PackageSupplier = supplier
+ case "PackageOriginator":
+ originator := &common.Originator{Originator: value}
+ if value == "NOASSERTION" {
+ parser.pkg.PackageOriginator = originator
+ break
+ }
+
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ switch subkey {
+ case "Person", "Organization":
+ originator.Originator = subvalue
+ originator.OriginatorType = subkey
+ default:
+ return fmt.Errorf("unrecognized PackageOriginator type %v", subkey)
+ }
+ parser.pkg.PackageOriginator = originator
+ case "PackageDownloadLocation":
+ parser.pkg.PackageDownloadLocation = value
+ case "FilesAnalyzed":
+ parser.pkg.IsFilesAnalyzedTagPresent = true
+ if value == "false" {
+ parser.pkg.FilesAnalyzed = false
+ } else if value == "true" {
+ parser.pkg.FilesAnalyzed = true
+ }
+ case "PackageVerificationCode":
+ parser.pkg.PackageVerificationCode = extractCodeAndExcludes(value)
+ case "PackageChecksum":
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ if parser.pkg.PackageChecksums == nil {
+ parser.pkg.PackageChecksums = []common.Checksum{}
+ }
+ switch common.ChecksumAlgorithm(subkey) {
+ case common.SHA1, common.SHA256, common.MD5:
+ algorithm := common.ChecksumAlgorithm(subkey)
+ parser.pkg.PackageChecksums = append(parser.pkg.PackageChecksums, common.Checksum{Algorithm: algorithm, Value: subvalue})
+ default:
+ return fmt.Errorf("got unknown checksum type %s", subkey)
+ }
+ case "PackageHomePage":
+ parser.pkg.PackageHomePage = value
+ case "PackageSourceInfo":
+ parser.pkg.PackageSourceInfo = value
+ case "PackageLicenseConcluded":
+ parser.pkg.PackageLicenseConcluded = value
+ case "PackageLicenseInfoFromFiles":
+ parser.pkg.PackageLicenseInfoFromFiles = append(parser.pkg.PackageLicenseInfoFromFiles, value)
+ case "PackageLicenseDeclared":
+ parser.pkg.PackageLicenseDeclared = value
+ case "PackageLicenseComments":
+ parser.pkg.PackageLicenseComments = value
+ case "PackageCopyrightText":
+ parser.pkg.PackageCopyrightText = value
+ case "PackageSummary":
+ parser.pkg.PackageSummary = value
+ case "PackageDescription":
+ parser.pkg.PackageDescription = value
+ case "PackageComment":
+ parser.pkg.PackageComment = value
+ case "PrimaryPackagePurpose":
+ parser.pkg.PrimaryPackagePurpose = value
+ case "ReleaseDate":
+ parser.pkg.ReleaseDate = value
+ case "BuiltDate":
+ parser.pkg.BuiltDate = value
+ case "ValidUntilDate":
+ parser.pkg.ValidUntilDate = value
+ case "PackageAttributionText":
+ parser.pkg.PackageAttributionTexts = append(parser.pkg.PackageAttributionTexts, value)
+ case "ExternalRef":
+ parser.pkgExtRef = &v2_3.PackageExternalReference{}
+ parser.pkg.PackageExternalReferences = append(parser.pkg.PackageExternalReferences, parser.pkgExtRef)
+ category, refType, locator, err := extractPackageExternalReference(value)
+ if err != nil {
+ return err
+ }
+ parser.pkgExtRef.Category = category
+ parser.pkgExtRef.RefType = refType
+ parser.pkgExtRef.Locator = locator
+ case "ExternalRefComment":
+ if parser.pkgExtRef == nil {
+ return fmt.Errorf("no current ExternalRef found")
+ }
+ parser.pkgExtRef.ExternalRefComment = value
+ // now, expire pkgExtRef anyway because it can have at most one comment
+ parser.pkgExtRef = nil
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Package section", tag)
+ }
+
+ return nil
+}
+
+// ===== Helper functions =====
+
+func extractCodeAndExcludes(value string) *common.PackageVerificationCode {
+ // FIXME this should probably be done using regular expressions instead
+ // split by paren + word "excludes:"
+ sp := strings.SplitN(value, "(excludes:", 2)
+ if len(sp) < 2 {
+ // not found; return the whole string as just the code
+ return &common.PackageVerificationCode{Value: value, ExcludedFiles: []string{}}
+ }
+
+ // if we're here, code is in first part and excludes filename is in
+ // second part, with trailing paren
+ code := strings.TrimSpace(sp[0])
+ parsedSp := strings.SplitN(sp[1], ")", 2)
+ fileName := strings.TrimSpace(parsedSp[0])
+ return &common.PackageVerificationCode{Value: code, ExcludedFiles: []string{fileName}}
+}
+
+func extractPackageExternalReference(value string) (string, string, string, error) {
+ sp := strings.Split(value, " ")
+ // remove any that are just whitespace
+ keepSp := []string{}
+ for _, s := range sp {
+ ss := strings.TrimSpace(s)
+ if ss != "" {
+ keepSp = append(keepSp, ss)
+ }
+ }
+ // now, should have 3 items and should be able to map them
+ if len(keepSp) != 3 {
+ return "", "", "", fmt.Errorf("expected 3 elements, got %d", len(keepSp))
+ }
+ return keepSp[0], keepSp[1], keepSp[2], nil
+}
diff --git a/tvloader/parser2v3/parse_package_test.go b/tvloader/parser2v3/parse_package_test.go
new file mode 100644
index 0000000..714e61f
--- /dev/null
+++ b/tvloader/parser2v3/parse_package_test.go
@@ -0,0 +1,1130 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser package section state change tests =====
+func TestParser2_3PackageStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ // create the first package
+ pkgOldName := "p1"
+
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: pkgOldName, PackageSPDXIdentifier: "p1"},
+ }
+ pkgOld := parser.pkg
+ parser.doc.Packages = append(parser.doc.Packages, pkgOld)
+ // the Document's Packages should have this one only
+ if parser.doc.Packages[0] != pkgOld {
+ t.Errorf("expected package %v, got %v", pkgOld, parser.doc.Packages[0])
+ }
+ if len(parser.doc.Packages) != 1 {
+ t.Errorf("expected 1 package, got %d", len(parser.doc.Packages))
+ }
+
+ // now add a new package
+ pkgName := "p2"
+ err := parser.parsePair2_3("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_3 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_3, parser.st)
+ }
+ // and a package should be created
+ if parser.pkg == nil {
+ t.Fatalf("parser didn't create new package")
+ }
+ // and it should not be pkgOld
+ if parser.pkg == pkgOld {
+ t.Errorf("expected new package, got pkgOld")
+ }
+ // and the package name should be as expected
+ if parser.pkg.PackageName != pkgName {
+ t.Errorf("expected package name %s, got %s", pkgName, parser.pkg.PackageName)
+ }
+ // and the package should default to true for FilesAnalyzed
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected FilesAnalyzed to default to true, got false")
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != false {
+ t.Errorf("expected IsFilesAnalyzedTagPresent to default to false, got true")
+ }
+ // and the Document's Packages should still be of size 1 and have pkgOld only
+ if parser.doc.Packages[0] != pkgOld {
+ t.Errorf("Expected package %v, got %v", pkgOld, parser.doc.Packages[0])
+ }
+ if len(parser.doc.Packages) != 1 {
+ t.Errorf("expected 1 package, got %d", len(parser.doc.Packages))
+ }
+}
+
+func TestParser2_3PackageStartsNewPackageAfterParsingPackageNameTagWhileInUnpackaged(t *testing.T) {
+ // pkg is nil, so that Files appearing before the first PackageName tag
+ // are added to Files instead of Packages
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psFile2_3,
+ pkg: nil,
+ }
+ // the Document's Packages should be empty
+ if len(parser.doc.Packages) != 0 {
+ t.Errorf("Expected zero packages, got %d", len(parser.doc.Packages))
+ }
+
+ // now add a new package
+ pkgName := "p2"
+ err := parser.parsePair2_3("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_3 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_3, parser.st)
+ }
+ // and a package should be created
+ if parser.pkg == nil {
+ t.Fatalf("parser didn't create new package")
+ }
+ // and the package name should be as expected
+ if parser.pkg.PackageName != pkgName {
+ t.Errorf("expected package name %s, got %s", pkgName, parser.pkg.PackageName)
+ }
+ // and the package should default to true for FilesAnalyzed
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected FilesAnalyzed to default to true, got false")
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != false {
+ t.Errorf("expected IsFilesAnalyzedTagPresent to default to false, got true")
+ }
+ // and the Document's Packages should be of size 0, because the prior was
+ // unpackaged files and this one won't be added until an SPDXID is seen
+ if len(parser.doc.Packages) != 0 {
+ t.Errorf("Expected %v packages in doc, got %v", 0, len(parser.doc.Packages))
+ }
+}
+
+func TestParser2_3PackageMovesToFileAfterParsingFileNameTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ pkgCurrent := parser.pkg
+
+ err := parser.parsePair2_3("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_3 {
+ t.Errorf("expected state to be %v, got %v", psFile2_3, parser.st)
+ }
+ // and current package should remain what it was
+ if parser.pkg != pkgCurrent {
+ t.Fatalf("expected package to remain %v, got %v", pkgCurrent, parser.pkg)
+ }
+}
+
+func TestParser2_3PackageMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_3("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+}
+
+func TestParser2_3PackageMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+}
+
+func TestParser2_3PackageStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psPackage2_3 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_3, parser.st)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psPackage2_3 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_3, parser.st)
+ }
+}
+
+func TestParser2_3PackageStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this package")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+}
+
+// ===== Package data section tests =====
+func TestParser2_3CanParsePackageTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // should not yet be in Packages map, b/c no SPDX identifier
+ if len(parser.doc.Packages) != 0 {
+ t.Errorf("expected 0 packages, got %d", len(parser.doc.Packages))
+ }
+
+ // Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageName != "p1" {
+ t.Errorf("got %v for PackageName", parser.pkg.PackageName)
+ }
+ // still should not yet be in Packages map, b/c no SPDX identifier
+ if len(parser.doc.Packages) != 0 {
+ t.Errorf("expected 0 packages, got %d", len(parser.doc.Packages))
+ }
+
+ // Package SPDX Identifier
+ err = parser.parsePairFromPackage2_3("SPDXID", "SPDXRef-p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // "SPDXRef-" prefix should be removed from the item
+ if parser.pkg.PackageSPDXIdentifier != "p1" {
+ t.Errorf("got %v for PackageSPDXIdentifier", parser.pkg.PackageSPDXIdentifier)
+ }
+ // and it should now be added to the Packages map
+ if len(parser.doc.Packages) != 1 {
+ t.Errorf("expected 1 package, got %d", len(parser.doc.Packages))
+ }
+ if parser.doc.Packages[0] != parser.pkg {
+ t.Errorf("expected to point to parser.pkg, got %v", parser.doc.Packages[0])
+ }
+
+ // Package Version
+ err = parser.parsePairFromPackage2_3("PackageVersion", "2.1.1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageVersion != "2.1.1" {
+ t.Errorf("got %v for PackageVersion", parser.pkg.PackageVersion)
+ }
+
+ // Package File Name
+ err = parser.parsePairFromPackage2_3("PackageFileName", "p1-2.1.1.tar.gz")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageFileName != "p1-2.1.1.tar.gz" {
+ t.Errorf("got %v for PackageFileName", parser.pkg.PackageFileName)
+ }
+
+ // Package Supplier
+ // SKIP -- separate tests for subvalues below
+
+ // Package Originator
+ // SKIP -- separate tests for subvalues below
+
+ // Package Download Location
+ err = parser.parsePairFromPackage2_3("PackageDownloadLocation", "https://example.com/whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageDownloadLocation != "https://example.com/whatever" {
+ t.Errorf("got %v for PackageDownloadLocation", parser.pkg.PackageDownloadLocation)
+ }
+
+ // Files Analyzed
+ err = parser.parsePairFromPackage2_3("FilesAnalyzed", "false")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.FilesAnalyzed != false {
+ t.Errorf("got %v for FilesAnalyzed", parser.pkg.FilesAnalyzed)
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != true {
+ t.Errorf("got %v for IsFilesAnalyzedTagPresent", parser.pkg.IsFilesAnalyzedTagPresent)
+ }
+
+ // Package Verification Code
+ // SKIP -- separate tests for "excludes", or not, below
+
+ // Package Checksums
+ codeSha1 := "85ed0817af83a24ad8da68c2b5094de69833983c"
+ sumSha1 := "SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c"
+ codeSha256 := "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ sumSha256 := "SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ codeMd5 := "624c1abb3664f4b35547e7c73864ad24"
+ sumMd5 := "MD5: 624c1abb3664f4b35547e7c73864ad24"
+ err = parser.parsePairFromPackage2_3("PackageChecksum", sumSha1)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_3("PackageChecksum", sumSha256)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_3("PackageChecksum", sumMd5)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ for _, checksum := range parser.pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != codeSha1 {
+ t.Errorf("expected %s for FileChecksumSHA1, got %s", codeSha1, checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != codeSha256 {
+ t.Errorf("expected %s for FileChecksumSHA1, got %s", codeSha256, checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != codeMd5 {
+ t.Errorf("expected %s for FileChecksumSHA1, got %s", codeMd5, checksum.Value)
+ }
+ }
+ }
+
+ // Package Home Page
+ err = parser.parsePairFromPackage2_3("PackageHomePage", "https://example.com/whatever2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageHomePage != "https://example.com/whatever2" {
+ t.Errorf("got %v for PackageHomePage", parser.pkg.PackageHomePage)
+ }
+
+ // Package Source Info
+ err = parser.parsePairFromPackage2_3("PackageSourceInfo", "random comment")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageSourceInfo != "random comment" {
+ t.Errorf("got %v for PackageSourceInfo", parser.pkg.PackageSourceInfo)
+ }
+
+ // Package License Concluded
+ err = parser.parsePairFromPackage2_3("PackageLicenseConcluded", "Apache-2.0 OR GPL-2.0-or-later")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageLicenseConcluded != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("got %v for PackageLicenseConcluded", parser.pkg.PackageLicenseConcluded)
+ }
+
+ // All Licenses Info From Files
+ lics := []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "CC0-1.0",
+ }
+ for _, lic := range lics {
+ err = parser.parsePairFromPackage2_3("PackageLicenseInfoFromFiles", lic)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, licWant := range lics {
+ flagFound := false
+ for _, licCheck := range parser.pkg.PackageLicenseInfoFromFiles {
+ if licWant == licCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in PackageLicenseInfoFromFiles", licWant)
+ }
+ }
+ if len(lics) != len(parser.pkg.PackageLicenseInfoFromFiles) {
+ t.Errorf("expected %d licenses in PackageLicenseInfoFromFiles, got %d", len(lics),
+ len(parser.pkg.PackageLicenseInfoFromFiles))
+ }
+
+ // Package License Declared
+ err = parser.parsePairFromPackage2_3("PackageLicenseDeclared", "Apache-2.0 OR GPL-2.0-or-later")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageLicenseDeclared != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("got %v for PackageLicenseDeclared", parser.pkg.PackageLicenseDeclared)
+ }
+
+ // Package License Comments
+ err = parser.parsePairFromPackage2_3("PackageLicenseComments", "this is a license comment")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageLicenseComments != "this is a license comment" {
+ t.Errorf("got %v for PackageLicenseComments", parser.pkg.PackageLicenseComments)
+ }
+
+ // Package Copyright Text
+ err = parser.parsePairFromPackage2_3("PackageCopyrightText", "Copyright (c) me myself and i")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageCopyrightText != "Copyright (c) me myself and i" {
+ t.Errorf("got %v for PackageCopyrightText", parser.pkg.PackageCopyrightText)
+ }
+
+ // Package Summary
+ err = parser.parsePairFromPackage2_3("PackageSummary", "i wrote this package")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageSummary != "i wrote this package" {
+ t.Errorf("got %v for PackageSummary", parser.pkg.PackageSummary)
+ }
+
+ // Package Description
+ err = parser.parsePairFromPackage2_3("PackageDescription", "i wrote this package a lot")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageDescription != "i wrote this package a lot" {
+ t.Errorf("got %v for PackageDescription", parser.pkg.PackageDescription)
+ }
+
+ // Package Comment
+ err = parser.parsePairFromPackage2_3("PackageComment", "i scanned this package")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageComment != "i scanned this package" {
+ t.Errorf("got %v for PackageComment", parser.pkg.PackageComment)
+ }
+
+ // Package Attribution Text
+ attrs := []string{
+ "Include this notice in all advertising materials",
+ "This is a \nmulti-line string",
+ }
+ for _, attr := range attrs {
+ err = parser.parsePairFromPackage2_3("PackageAttributionText", attr)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, attrWant := range attrs {
+ flagFound := false
+ for _, attrCheck := range parser.pkg.PackageAttributionTexts {
+ if attrWant == attrCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in PackageAttributionText", attrWant)
+ }
+ }
+ if len(attrs) != len(parser.pkg.PackageAttributionTexts) {
+ t.Errorf("expected %d attribution texts in PackageAttributionTexts, got %d", len(attrs),
+ len(parser.pkg.PackageAttributionTexts))
+ }
+
+ // Package External References and Comments
+ ref1 := "SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+ ref1Category := "SECURITY"
+ ref1Type := "cpe23Type"
+ ref1Locator := "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+ ref1Comment := "this is comment #1"
+ ref2 := "OTHER LocationRef-acmeforge acmecorp/acmenator/4.1.3alpha"
+ ref2Category := "OTHER"
+ ref2Type := "LocationRef-acmeforge"
+ ref2Locator := "acmecorp/acmenator/4.1.3alpha"
+ ref2Comment := "this is comment #2"
+ err = parser.parsePairFromPackage2_3("ExternalRef", ref1)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.pkg.PackageExternalReferences) != 1 {
+ t.Errorf("expected 1 external reference, got %d", len(parser.pkg.PackageExternalReferences))
+ }
+ if parser.pkgExtRef == nil {
+ t.Errorf("expected non-nil pkgExtRef, got nil")
+ }
+ if parser.pkg.PackageExternalReferences[0] == nil {
+ t.Errorf("expected non-nil PackageExternalReferences[0], got nil")
+ }
+ if parser.pkgExtRef != parser.pkg.PackageExternalReferences[0] {
+ t.Errorf("expected pkgExtRef to match PackageExternalReferences[0], got no match")
+ }
+ err = parser.parsePairFromPackage2_3("ExternalRefComment", ref1Comment)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_3("ExternalRef", ref2)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.pkg.PackageExternalReferences) != 2 {
+ t.Errorf("expected 2 external references, got %d", len(parser.pkg.PackageExternalReferences))
+ }
+ if parser.pkgExtRef == nil {
+ t.Errorf("expected non-nil pkgExtRef, got nil")
+ }
+ if parser.pkg.PackageExternalReferences[1] == nil {
+ t.Errorf("expected non-nil PackageExternalReferences[1], got nil")
+ }
+ if parser.pkgExtRef != parser.pkg.PackageExternalReferences[1] {
+ t.Errorf("expected pkgExtRef to match PackageExternalReferences[1], got no match")
+ }
+ err = parser.parsePairFromPackage2_3("ExternalRefComment", ref2Comment)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // finally, check these values
+ gotRef1 := parser.pkg.PackageExternalReferences[0]
+ if gotRef1.Category != ref1Category {
+ t.Errorf("expected ref1 category to be %s, got %s", gotRef1.Category, ref1Category)
+ }
+ if gotRef1.RefType != ref1Type {
+ t.Errorf("expected ref1 type to be %s, got %s", gotRef1.RefType, ref1Type)
+ }
+ if gotRef1.Locator != ref1Locator {
+ t.Errorf("expected ref1 locator to be %s, got %s", gotRef1.Locator, ref1Locator)
+ }
+ if gotRef1.ExternalRefComment != ref1Comment {
+ t.Errorf("expected ref1 comment to be %s, got %s", gotRef1.ExternalRefComment, ref1Comment)
+ }
+ gotRef2 := parser.pkg.PackageExternalReferences[1]
+ if gotRef2.Category != ref2Category {
+ t.Errorf("expected ref2 category to be %s, got %s", gotRef2.Category, ref2Category)
+ }
+ if gotRef2.RefType != ref2Type {
+ t.Errorf("expected ref2 type to be %s, got %s", gotRef2.RefType, ref2Type)
+ }
+ if gotRef2.Locator != ref2Locator {
+ t.Errorf("expected ref2 locator to be %s, got %s", gotRef2.Locator, ref2Locator)
+ }
+ if gotRef2.ExternalRefComment != ref2Comment {
+ t.Errorf("expected ref2 comment to be %s, got %s", gotRef2.ExternalRefComment, ref2Comment)
+ }
+
+}
+
+func TestParser2_3CanParsePackageSupplierPersonTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: Person
+ err := parser.parsePairFromPackage2_3("PackageSupplier", "Person: John Doe")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageSupplier.Supplier != "John Doe" {
+ t.Errorf("got %v for PackageSupplierPerson", parser.pkg.PackageSupplier.Supplier)
+ }
+}
+
+func TestParser2_3CanParsePackageSupplierOrganizationTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: Organization
+ err := parser.parsePairFromPackage2_3("PackageSupplier", "Organization: John Doe, Inc.")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageSupplier.Supplier != "John Doe, Inc." {
+ t.Errorf("got %v for PackageSupplierOrganization", parser.pkg.PackageSupplier.Supplier)
+ }
+}
+
+func TestParser2_3CanParsePackageSupplierNOASSERTIONTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: NOASSERTION
+ err := parser.parsePairFromPackage2_3("PackageSupplier", "NOASSERTION")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("got value for Supplier, expected NOASSERTION")
+ }
+}
+
+func TestParser2_3CanParsePackageOriginatorPersonTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: Person
+ err := parser.parsePairFromPackage2_3("PackageOriginator", "Person: John Doe")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageOriginator.Originator != "John Doe" {
+ t.Errorf("got %v for PackageOriginator", parser.pkg.PackageOriginator.Originator)
+ }
+}
+
+func TestParser2_3CanParsePackageOriginatorOrganizationTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: Organization
+ err := parser.parsePairFromPackage2_3("PackageOriginator", "Organization: John Doe, Inc.")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageOriginator.Originator != "John Doe, Inc." {
+ t.Errorf("got %v for PackageOriginator", parser.pkg.PackageOriginator.Originator)
+ }
+}
+
+func TestParser2_3CanParsePackageOriginatorNOASSERTIONTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: NOASSERTION
+ err := parser.parsePairFromPackage2_3("PackageOriginator", "NOASSERTION")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("got false for PackageOriginatorNOASSERTION")
+ }
+}
+
+func TestParser2_3CanParsePackageVerificationCodeTagWithExcludes(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Verification Code with excludes parenthetical
+ code := "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ fileName := "./package.spdx"
+ fullCodeValue := "d6a770ba38583ed4bb4525bd96e50461655d2758 (excludes: ./package.spdx)"
+ err := parser.parsePairFromPackage2_3("PackageVerificationCode", fullCodeValue)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageVerificationCode.Value != code {
+ t.Errorf("got %v for PackageVerificationCode", parser.pkg.PackageVerificationCode)
+ }
+ if len(parser.pkg.PackageVerificationCode.ExcludedFiles) != 1 || parser.pkg.PackageVerificationCode.ExcludedFiles[0] != fileName {
+ t.Errorf("got %v for PackageVerificationCodeExcludedFile", parser.pkg.PackageVerificationCode.ExcludedFiles)
+ }
+
+}
+
+func TestParser2_3CanParsePackageVerificationCodeTagWithoutExcludes(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Verification Code without excludes parenthetical
+ code := "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ err := parser.parsePairFromPackage2_3("PackageVerificationCode", code)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageVerificationCode.Value != code {
+ t.Errorf("got %v for PackageVerificationCode", parser.pkg.PackageVerificationCode)
+ }
+ if len(parser.pkg.PackageVerificationCode.ExcludedFiles) != 0 {
+ t.Errorf("got %v for PackageVerificationCodeExcludedFile", parser.pkg.PackageVerificationCode.ExcludedFiles)
+ }
+
+}
+
+func TestParser2_3PackageExternalRefPointerChangesAfterTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ ref1 := "SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+ err := parser.parsePairFromPackage2_3("ExternalRef", ref1)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkgExtRef == nil {
+ t.Errorf("expected non-nil external reference pointer, got nil")
+ }
+
+ // now, a comment; pointer should go away
+ err = parser.parsePairFromPackage2_3("ExternalRefComment", "whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkgExtRef != nil {
+ t.Errorf("expected nil external reference pointer, got non-nil")
+ }
+
+ ref2 := "Other LocationRef-something https://example.com/whatever"
+ err = parser.parsePairFromPackage2_3("ExternalRef", ref2)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkgExtRef == nil {
+ t.Errorf("expected non-nil external reference pointer, got nil")
+ }
+
+ // and some other random tag makes the pointer go away too
+ err = parser.parsePairFromPackage2_3("PackageSummary", "whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkgExtRef != nil {
+ t.Errorf("expected nil external reference pointer, got non-nil")
+ }
+}
+
+func TestParser2_3PackageCreatesRelationshipInDocument(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.rln == nil {
+ t.Fatalf("parser didn't create and point to Relationship struct")
+ }
+ if parser.rln != parser.doc.Relationships[0] {
+ t.Errorf("pointer to new Relationship doesn't match idx 0 for doc.Relationships[]")
+ }
+}
+
+func TestParser2_3PackageCreatesAnnotationInDocument(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.ann == nil {
+ t.Fatalf("parser didn't create and point to Annotation struct")
+ }
+ if parser.ann != parser.doc.Annotations[0] {
+ t.Errorf("pointer to new Annotation doesn't match idx 0 for doc.Annotations[]")
+ }
+}
+
+func TestParser2_3PackageUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePairFromPackage2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_3FailsIfInvalidSPDXIDInPackageSection(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid ID format
+ err = parser.parsePairFromPackage2_3("SPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfInvalidPackageSupplierFormat(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid supplier format
+ err = parser.parsePairFromPackage2_3("PackageSupplier", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfUnknownPackageSupplierType(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid supplier type
+ err = parser.parsePairFromPackage2_3("PackageSupplier", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfInvalidPackageOriginatorFormat(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid originator format
+ err = parser.parsePairFromPackage2_3("PackageOriginator", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfUnknownPackageOriginatorType(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid originator type
+ err = parser.parsePairFromPackage2_3("PackageOriginator", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3SetsFilesAnalyzedTagsCorrectly(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // set tag
+ err = parser.parsePairFromPackage2_3("FilesAnalyzed", "true")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, parser.pkg.FilesAnalyzed)
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != true {
+ t.Errorf("expected %v, got %v", true, parser.pkg.IsFilesAnalyzedTagPresent)
+ }
+}
+
+func TestParser2_3FailsIfInvalidPackageChecksumFormat(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid checksum format
+ err = parser.parsePairFromPackage2_3("PackageChecksum", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfInvalidPackageChecksumType(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid checksum type
+ err = parser.parsePairFromPackage2_3("PackageChecksum", "whoops: blah")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfInvalidExternalRefFormat(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid external ref format
+ err = parser.parsePairFromPackage2_3("ExternalRef", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfExternalRefCommentBeforeExternalRef(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_3("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // external ref comment before external ref
+ err = parser.parsePairFromPackage2_3("ExternalRefComment", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+// ===== Helper function tests =====
+
+func TestCanCheckAndExtractExcludesFilenameAndCode(t *testing.T) {
+ code := "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ fileName := "./package.spdx"
+ fullCodeValue := "d6a770ba38583ed4bb4525bd96e50461655d2758 (excludes: ./package.spdx)"
+
+ gotCode := extractCodeAndExcludes(fullCodeValue)
+ if gotCode.Value != code {
+ t.Errorf("got %v for gotCode", gotCode)
+ }
+ if len(gotCode.ExcludedFiles) != 1 || gotCode.ExcludedFiles[0] != fileName {
+ t.Errorf("got %v for gotFileName", gotCode.ExcludedFiles)
+ }
+}
+
+func TestCanExtractPackageExternalReference(t *testing.T) {
+ ref1 := "SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+ category := "SECURITY"
+ refType := "cpe23Type"
+ location := "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+
+ gotCategory, gotRefType, gotLocation, err := extractPackageExternalReference(ref1)
+ if err != nil {
+ t.Errorf("got non-nil error: %v", err)
+ }
+ if gotCategory != category {
+ t.Errorf("expected category %s, got %s", category, gotCategory)
+ }
+ if gotRefType != refType {
+ t.Errorf("expected refType %s, got %s", refType, gotRefType)
+ }
+ if gotLocation != location {
+ t.Errorf("expected location %s, got %s", location, gotLocation)
+ }
+}
+
+func TestCanExtractPackageExternalReferenceWithExtraWhitespace(t *testing.T) {
+ ref1 := " SECURITY \t cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:* \t "
+ category := "SECURITY"
+ refType := "cpe23Type"
+ location := "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+
+ gotCategory, gotRefType, gotLocation, err := extractPackageExternalReference(ref1)
+ if err != nil {
+ t.Errorf("got non-nil error: %v", err)
+ }
+ if gotCategory != category {
+ t.Errorf("expected category %s, got %s", category, gotCategory)
+ }
+ if gotRefType != refType {
+ t.Errorf("expected refType %s, got %s", refType, gotRefType)
+ }
+ if gotLocation != location {
+ t.Errorf("expected location %s, got %s", location, gotLocation)
+ }
+}
+
+func TestFailsPackageExternalRefWithInvalidFormat(t *testing.T) {
+ _, _, _, err := extractPackageExternalReference("whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3PackageWithoutSpdxIdentifierThrowsError(t *testing.T) {
+ // More than one package, the previous package doesn't contain an SPDX ID
+ pkgOldName := "p1"
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psPackage2_3,
+ pkg: &v2_3.Package{PackageName: pkgOldName},
+ }
+ pkgOld := parser.pkg
+ parser.doc.Packages = append(parser.doc.Packages, pkgOld)
+ // the Document's Packages should have this one only
+ if parser.doc.Packages[0] != pkgOld {
+ t.Errorf("expected package %v, got %v", pkgOld, parser.doc.Packages[0])
+ }
+ if len(parser.doc.Packages) != 1 {
+ t.Errorf("expected 1 package, got %d", len(parser.doc.Packages))
+ }
+
+ pkgName := "p2"
+ err := parser.parsePair2_3("PackageName", pkgName)
+ if err == nil {
+ t.Errorf("package without SPDX Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v3/parse_relationship.go b/tvloader/parser2v3/parse_relationship.go
new file mode 100644
index 0000000..8f49417
--- /dev/null
+++ b/tvloader/parser2v3/parse_relationship.go
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+)
+
+func (parser *tvParser2_3) parsePairForRelationship2_3(tag string, value string) error {
+ if parser.rln == nil {
+ return fmt.Errorf("no relationship struct created in parser rln pointer")
+ }
+
+ if tag == "Relationship" {
+ // parse the value to see if it's a valid relationship format
+ sp := strings.SplitN(value, " ", -1)
+
+ // filter out any purely-whitespace items
+ var rp []string
+ for _, v := range sp {
+ v = strings.TrimSpace(v)
+ if v != "" {
+ rp = append(rp, v)
+ }
+ }
+
+ if len(rp) != 3 {
+ return fmt.Errorf("invalid relationship format for %s", value)
+ }
+
+ aID, err := extractDocElementID(strings.TrimSpace(rp[0]))
+ if err != nil {
+ return err
+ }
+ parser.rln.RefA = aID
+ parser.rln.Relationship = strings.TrimSpace(rp[1])
+ // NONE and NOASSERTION are permitted on right side
+ permittedSpecial := []string{"NONE", "NOASSERTION"}
+ bID, err := extractDocElementSpecial(strings.TrimSpace(rp[2]), permittedSpecial)
+ if err != nil {
+ return err
+ }
+ parser.rln.RefB = bID
+ return nil
+ }
+
+ if tag == "RelationshipComment" {
+ parser.rln.RelationshipComment = value
+ return nil
+ }
+
+ return fmt.Errorf("received unknown tag %v in Relationship section", tag)
+}
diff --git a/tvloader/parser2v3/parse_relationship_test.go b/tvloader/parser2v3/parse_relationship_test.go
new file mode 100644
index 0000000..57714d1
--- /dev/null
+++ b/tvloader/parser2v3/parse_relationship_test.go
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Relationship section tests =====
+func TestParser2_3FailsIfRelationshipNotSet(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePairForRelationship2_3("Relationship", "SPDXRef-A CONTAINS SPDXRef-B")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromRelationship2_3 without setting rln pointer")
+ }
+}
+
+func TestParser2_3FailsIfRelationshipCommentWithoutRelationship(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("RelationshipComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for RelationshipComment without Relationship first")
+ }
+}
+
+func TestParser2_3CanParseRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // Relationship
+ err := parser.parsePair2_3("Relationship", "SPDXRef-something CONTAINS DocumentRef-otherdoc:SPDXRef-something-else")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rln.RefA.DocumentRefID != "" || parser.rln.RefA.ElementRefID != "something" {
+ t.Errorf("got %v for first part of Relationship, expected something", parser.rln.RefA)
+ }
+ if parser.rln.RefB.DocumentRefID != "otherdoc" || parser.rln.RefB.ElementRefID != "something-else" {
+ t.Errorf("got %v for second part of Relationship, expected otherdoc:something-else", parser.rln.RefB)
+ }
+ if parser.rln.Relationship != "CONTAINS" {
+ t.Errorf("got %v for Relationship type, expected CONTAINS", parser.rln.Relationship)
+ }
+
+ // Relationship Comment
+ cmt := "this is a comment"
+ err = parser.parsePair2_3("RelationshipComment", cmt)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rln.RelationshipComment != cmt {
+ t.Errorf("got %v for RelationshipComment, expected %v", parser.rln.RelationshipComment, cmt)
+ }
+}
+
+func TestParser2_3InvalidRelationshipTagsNoValueFail(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // no items
+ parser.rln = nil
+ err := parser.parsePair2_3("Relationship", "")
+ if err == nil {
+ t.Errorf("expected error for empty items in relationship, got nil")
+ }
+}
+
+func TestParser2_3InvalidRelationshipTagsOneValueFail(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // one item
+ parser.rln = nil
+ err := parser.parsePair2_3("Relationship", "DESCRIBES")
+ if err == nil {
+ t.Errorf("expected error for only one item in relationship, got nil")
+ }
+}
+
+func TestParser2_3InvalidRelationshipTagsTwoValuesFail(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // two items
+ parser.rln = nil
+ err := parser.parsePair2_3("Relationship", "SPDXRef-DOCUMENT DESCRIBES")
+ if err == nil {
+ t.Errorf("expected error for only two items in relationship, got nil")
+ }
+}
+
+func TestParser2_3InvalidRelationshipTagsThreeValuesSucceed(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // three items but with interspersed additional whitespace
+ parser.rln = nil
+ err := parser.parsePair2_3("Relationship", " SPDXRef-DOCUMENT \t DESCRIBES SPDXRef-something-else ")
+ if err != nil {
+ t.Errorf("expected pass for three items in relationship w/ extra whitespace, got: %v", err)
+ }
+}
+
+func TestParser2_3InvalidRelationshipTagsFourValuesFail(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // four items
+ parser.rln = nil
+ err := parser.parsePair2_3("Relationship", "SPDXRef-a DESCRIBES SPDXRef-b SPDXRef-c")
+ if err == nil {
+ t.Errorf("expected error for more than three items in relationship, got nil")
+ }
+}
+
+func TestParser2_3InvalidRelationshipTagsInvalidRefIDs(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // four items
+ parser.rln = nil
+ err := parser.parsePair2_3("Relationship", "SPDXRef-a DESCRIBES b")
+ if err == nil {
+ t.Errorf("expected error for missing SPDXRef- prefix, got nil")
+ }
+
+ parser.rln = nil
+ err = parser.parsePair2_3("Relationship", "a DESCRIBES SPDXRef-b")
+ if err == nil {
+ t.Errorf("expected error for missing SPDXRef- prefix, got nil")
+ }
+}
+
+func TestParser2_3SpecialValuesValidForRightSideOfRelationship(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // NONE in right side of relationship should pass
+ err := parser.parsePair2_3("Relationship", "SPDXRef-a CONTAINS NONE")
+ if err != nil {
+ t.Errorf("expected nil error for CONTAINS NONE, got %v", err)
+ }
+
+ // NOASSERTION in right side of relationship should pass
+ err = parser.parsePair2_3("Relationship", "SPDXRef-a CONTAINS NOASSERTION")
+ if err != nil {
+ t.Errorf("expected nil error for CONTAINS NOASSERTION, got %v", err)
+ }
+
+ // NONE in left side of relationship should fail
+ err = parser.parsePair2_3("Relationship", "NONE CONTAINS SPDXRef-a")
+ if err == nil {
+ t.Errorf("expected non-nil error for NONE CONTAINS, got nil")
+ }
+
+ // NOASSERTION in left side of relationship should fail
+ err = parser.parsePair2_3("Relationship", "NOASSERTION CONTAINS SPDXRef-a")
+ if err == nil {
+ t.Errorf("expected non-nil error for NOASSERTION CONTAINS, got nil")
+ }
+}
+
+func TestParser2_3FailsToParseUnknownTagInRelationshipSection(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // Relationship
+ err := parser.parsePair2_3("Relationship", "SPDXRef-something CONTAINS DocumentRef-otherdoc:SPDXRef-something-else")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid tag
+ err = parser.parsePairForRelationship2_3("blah", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v3/parse_review.go b/tvloader/parser2v3/parse_review.go
new file mode 100644
index 0000000..c7ff99c
--- /dev/null
+++ b/tvloader/parser2v3/parse_review.go
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromReview2_3(tag string, value string) error {
+ switch tag {
+ // tag for creating new review section
+ case "Reviewer":
+ parser.rev = &v2_3.Review{}
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ switch subkey {
+ case "Person":
+ parser.rev.Reviewer = subvalue
+ parser.rev.ReviewerType = "Person"
+ case "Organization":
+ parser.rev.Reviewer = subvalue
+ parser.rev.ReviewerType = "Organization"
+ case "Tool":
+ parser.rev.Reviewer = subvalue
+ parser.rev.ReviewerType = "Tool"
+ default:
+ return fmt.Errorf("unrecognized Reviewer type %v", subkey)
+ }
+ case "ReviewDate":
+ parser.rev.ReviewDate = value
+ case "ReviewComment":
+ parser.rev.ReviewComment = value
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Review section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_review_test.go b/tvloader/parser2v3/parse_review_test.go
new file mode 100644
index 0000000..7026a36
--- /dev/null
+++ b/tvloader/parser2v3/parse_review_test.go
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser review section state change tests =====
+func TestParser2_3ReviewStartsNewReviewAfterParsingReviewerTag(t *testing.T) {
+ // create the first review
+ rev1 := "John Doe"
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{
+ Reviewer: rev1,
+ ReviewerType: "Person",
+ },
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+ r1 := parser.rev
+
+ // the Document's Reviews should have this one only
+ if len(parser.doc.Reviews) != 1 {
+ t.Errorf("Expected only one review, got %d", len(parser.doc.Reviews))
+ }
+ if parser.doc.Reviews[0] != r1 {
+ t.Errorf("Expected review %v in Reviews[0], got %v", r1, parser.doc.Reviews[0])
+ }
+ if parser.doc.Reviews[0].Reviewer != rev1 {
+ t.Errorf("expected review name %s in Reviews[0], got %s", rev1, parser.doc.Reviews[0].Reviewer)
+ }
+
+ // now add a new review
+ rev2 := "Steve"
+ rp2 := "Person: Steve"
+ err := parser.parsePair2_3("Reviewer", rp2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+ // and a review should be created
+ if parser.rev == nil {
+ t.Fatalf("parser didn't create new review")
+ }
+ // and the reviewer's name should be as expected
+ if parser.rev.Reviewer != rev2 {
+ t.Errorf("expected reviewer name %s, got %s", rev2, parser.rev.Reviewer)
+ }
+ // and the Document's reviews should be of size 2 and have these two
+ if len(parser.doc.Reviews) != 2 {
+ t.Fatalf("Expected Reviews to have len 2, got %d", len(parser.doc.Reviews))
+ }
+ if parser.doc.Reviews[0] != r1 {
+ t.Errorf("Expected review %v in Reviews[0], got %v", r1, parser.doc.Reviews[0])
+ }
+ if parser.doc.Reviews[0].Reviewer != rev1 {
+ t.Errorf("expected reviewer name %s in Reviews[0], got %s", rev1, parser.doc.Reviews[0].Reviewer)
+ }
+ if parser.doc.Reviews[1] != parser.rev {
+ t.Errorf("Expected review %v in Reviews[1], got %v", parser.rev, parser.doc.Reviews[1])
+ }
+ if parser.doc.Reviews[1].Reviewer != rev2 {
+ t.Errorf("expected reviewer name %s in Reviews[1], got %s", rev2, parser.doc.Reviews[1].Reviewer)
+ }
+
+}
+
+func TestParser2_3ReviewStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{
+ Reviewer: "Jane Doe",
+ ReviewerType: "Person",
+ },
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+ // and the relationship should be in the Document's Relationships
+ if len(parser.doc.Relationships) != 1 {
+ t.Fatalf("expected doc.Relationships to have len 1, got %d", len(parser.doc.Relationships))
+ }
+ deID := parser.doc.Relationships[0].RefA
+ if deID.DocumentRefID != "" || deID.ElementRefID != "blah" {
+ t.Errorf("expected RefA to be %s, got %s", "blah", parser.doc.Relationships[0].RefA)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+}
+
+func TestParser2_3ReviewStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{
+ Reviewer: "Jane Doe",
+ ReviewerType: "Person",
+ },
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+
+ // and the annotation should be in the Document's Annotations
+ if len(parser.doc.Annotations) != 1 {
+ t.Fatalf("expected doc.Annotations to have len 1, got %d", len(parser.doc.Annotations))
+ }
+ if parser.doc.Annotations[0].Annotator.Annotator != "John Doe ()" {
+ t.Errorf("expected Annotator to be %s, got %s", "John Doe ()", parser.doc.Annotations[0].Annotator)
+ }
+}
+
+func TestParser2_3ReviewFailsAfterParsingOtherSectionTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ // can't go back to old sections
+ err := parser.parsePair2_3("SPDXVersion", "SPDX-2.3")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+ err = parser.parsePair2_3("PackageName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+ err = parser.parsePair2_3("FileName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+ err = parser.parsePair2_3("LicenseID", "LicenseRef-Lic22")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3, got nil")
+ }
+}
+
+// ===== Review data section tests =====
+func TestParser2_3CanParseReviewTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ // Reviewer (DEPRECATED)
+ // handled in subsequent subtests
+
+ // Review Date (DEPRECATED)
+ err := parser.parsePairFromReview2_3("ReviewDate", "2018-09-23T08:30:00Z")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rev.ReviewDate != "2018-09-23T08:30:00Z" {
+ t.Errorf("got %v for ReviewDate", parser.rev.ReviewDate)
+ }
+
+ // Review Comment (DEPRECATED)
+ err = parser.parsePairFromReview2_3("ReviewComment", "this is a comment")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rev.ReviewComment != "this is a comment" {
+ t.Errorf("got %v for ReviewComment", parser.rev.ReviewComment)
+ }
+}
+
+func TestParser2_3CanParseReviewerPersonTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ // Reviewer: Person
+ err := parser.parsePairFromReview2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rev.Reviewer != "John Doe" {
+ t.Errorf("got %v for Reviewer", parser.rev.Reviewer)
+ }
+ if parser.rev.ReviewerType != "Person" {
+ t.Errorf("got %v for ReviewerType", parser.rev.ReviewerType)
+ }
+}
+
+func TestParser2_3CanParseReviewerOrganizationTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ // Reviewer: Organization
+ err := parser.parsePairFromReview2_3("Reviewer", "Organization: John Doe, Inc.")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rev.Reviewer != "John Doe, Inc." {
+ t.Errorf("got %v for Reviewer", parser.rev.Reviewer)
+ }
+ if parser.rev.ReviewerType != "Organization" {
+ t.Errorf("got %v for ReviewerType", parser.rev.ReviewerType)
+ }
+}
+
+func TestParser2_3CanParseReviewerToolTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ // Reviewer: Tool
+ err := parser.parsePairFromReview2_3("Reviewer", "Tool: scannertool - 1.2.12")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.rev.Reviewer != "scannertool - 1.2.12" {
+ t.Errorf("got %v for Reviewer", parser.rev.Reviewer)
+ }
+ if parser.rev.ReviewerType != "Tool" {
+ t.Errorf("got %v for ReviewerType", parser.rev.ReviewerType)
+ }
+}
+
+func TestParser2_3FailsIfReviewerInvalidFormat(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_3("Reviewer", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfReviewerUnknownType(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_3("Reviewer", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3ReviewUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psReview2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_3.Review{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, parser.otherLic)
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
diff --git a/tvloader/parser2v3/parse_snippet.go b/tvloader/parser2v3/parse_snippet.go
new file mode 100644
index 0000000..d718dd0
--- /dev/null
+++ b/tvloader/parser2v3/parse_snippet.go
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromSnippet2_3(tag string, value string) error {
+ switch tag {
+ // tag for creating new snippet section
+ case "SnippetSPDXID":
+ // check here whether the file contained an SPDX ID or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_3 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.snippet = &v2_3.Snippet{}
+ eID, err := extractElementID(value)
+ if err != nil {
+ return err
+ }
+ // FIXME: how should we handle where not associated with current file?
+ if parser.file != nil {
+ if parser.file.Snippets == nil {
+ parser.file.Snippets = map[common.ElementID]*v2_3.Snippet{}
+ }
+ parser.file.Snippets[eID] = parser.snippet
+ }
+ parser.snippet.SnippetSPDXIdentifier = eID
+ // tag for creating new file section and going back to parsing File
+ case "FileName":
+ parser.st = psFile2_3
+ parser.snippet = nil
+ return parser.parsePairFromFile2_3(tag, value)
+ // tag for creating new package section and going back to parsing Package
+ case "PackageName":
+ parser.st = psPackage2_3
+ parser.file = nil
+ parser.snippet = nil
+ return parser.parsePairFromPackage2_3(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_3
+ return parser.parsePairFromOtherLicense2_3(tag, value)
+ // tags for snippet data
+ case "SnippetFromFileSPDXID":
+ deID, err := extractDocElementID(value)
+ if err != nil {
+ return err
+ }
+ parser.snippet.SnippetFromFileSPDXIdentifier = deID.ElementRefID
+ case "SnippetByteRange":
+ byteStart, byteEnd, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ bIntStart, err := strconv.Atoi(byteStart)
+ if err != nil {
+ return err
+ }
+ bIntEnd, err := strconv.Atoi(byteEnd)
+ if err != nil {
+ return err
+ }
+
+ if parser.snippet.Ranges == nil {
+ parser.snippet.Ranges = []common.SnippetRange{}
+ }
+ byteRange := common.SnippetRange{StartPointer: common.SnippetRangePointer{Offset: bIntStart}, EndPointer: common.SnippetRangePointer{Offset: bIntEnd}}
+ parser.snippet.Ranges = append(parser.snippet.Ranges, byteRange)
+ case "SnippetLineRange":
+ lineStart, lineEnd, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ lInttStart, err := strconv.Atoi(lineStart)
+ if err != nil {
+ return err
+ }
+ lInttEnd, err := strconv.Atoi(lineEnd)
+ if err != nil {
+ return err
+ }
+
+ if parser.snippet.Ranges == nil {
+ parser.snippet.Ranges = []common.SnippetRange{}
+ }
+ lineRange := common.SnippetRange{StartPointer: common.SnippetRangePointer{LineNumber: lInttStart}, EndPointer: common.SnippetRangePointer{LineNumber: lInttEnd}}
+ parser.snippet.Ranges = append(parser.snippet.Ranges, lineRange)
+ case "SnippetLicenseConcluded":
+ parser.snippet.SnippetLicenseConcluded = value
+ case "LicenseInfoInSnippet":
+ parser.snippet.LicenseInfoInSnippet = append(parser.snippet.LicenseInfoInSnippet, value)
+ case "SnippetLicenseComments":
+ parser.snippet.SnippetLicenseComments = value
+ case "SnippetCopyrightText":
+ parser.snippet.SnippetCopyrightText = value
+ case "SnippetComment":
+ parser.snippet.SnippetComment = value
+ case "SnippetName":
+ parser.snippet.SnippetName = value
+ case "SnippetAttributionText":
+ parser.snippet.SnippetAttributionTexts = append(parser.snippet.SnippetAttributionTexts, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Snippet section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_snippet_test.go b/tvloader/parser2v3/parse_snippet_test.go
new file mode 100644
index 0000000..362f22c
--- /dev/null
+++ b/tvloader/parser2v3/parse_snippet_test.go
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser snippet section state change tests =====
+func TestParser2_3SnippetStartsNewSnippetAfterParsingSnippetSPDXIDTag(t *testing.T) {
+ // create the first snippet
+ sid1 := common.ElementID("s1")
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: sid1},
+ }
+ s1 := parser.snippet
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets[sid1] = parser.snippet
+
+ // the File's Snippets should have this one only
+ if len(parser.file.Snippets) != 1 {
+ t.Errorf("Expected len(Snippets) to be 1, got %d", len(parser.file.Snippets))
+ }
+ if parser.file.Snippets["s1"] != s1 {
+ t.Errorf("Expected snippet %v in Snippets[s1], got %v", s1, parser.file.Snippets["s1"])
+ }
+ if parser.file.Snippets["s1"].SnippetSPDXIdentifier != sid1 {
+ t.Errorf("expected snippet ID %s in Snippets[s1], got %s", sid1, parser.file.Snippets["s1"].SnippetSPDXIdentifier)
+ }
+
+ // now add a new snippet
+ err := parser.parsePair2_3("SnippetSPDXID", "SPDXRef-s2")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psSnippet2_3 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_3, parser.st)
+ }
+ // and a snippet should be created
+ if parser.snippet == nil {
+ t.Fatalf("parser didn't create new snippet")
+ }
+ // and the snippet ID should be as expected
+ if parser.snippet.SnippetSPDXIdentifier != "s2" {
+ t.Errorf("expected snippet ID %s, got %s", "s2", parser.snippet.SnippetSPDXIdentifier)
+ }
+ // and the File's Snippets should be of size 2 and have these two
+ if len(parser.file.Snippets) != 2 {
+ t.Errorf("Expected len(Snippets) to be 2, got %d", len(parser.file.Snippets))
+ }
+ if parser.file.Snippets["s1"] != s1 {
+ t.Errorf("Expected snippet %v in Snippets[s1], got %v", s1, parser.file.Snippets["s1"])
+ }
+ if parser.file.Snippets["s1"].SnippetSPDXIdentifier != sid1 {
+ t.Errorf("expected snippet ID %s in Snippets[s1], got %s", sid1, parser.file.Snippets["s1"].SnippetSPDXIdentifier)
+ }
+ if parser.file.Snippets["s2"] != parser.snippet {
+ t.Errorf("Expected snippet %v in Snippets[s2], got %v", parser.snippet, parser.file.Snippets["s2"])
+ }
+ if parser.file.Snippets["s2"].SnippetSPDXIdentifier != "s2" {
+ t.Errorf("expected snippet ID %s in Snippets[s2], got %s", "s2", parser.file.Snippets["s2"].SnippetSPDXIdentifier)
+ }
+}
+
+func TestParser2_3SnippetStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ p1 := parser.pkg
+ f1 := parser.file
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets["s1"] = parser.snippet
+
+ // now add a new package
+ p2Name := "package2"
+ err := parser.parsePair2_3("PackageName", p2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should go back to Package
+ if parser.st != psPackage2_3 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_3, parser.st)
+ }
+ // and a package should be created
+ if parser.pkg == nil {
+ t.Fatalf("parser didn't create new pkg")
+ }
+ // and the package name should be as expected
+ if parser.pkg.PackageName != p2Name {
+ t.Errorf("expected package name %s, got %s", p2Name, parser.pkg.PackageName)
+ }
+ // and the package should default to true for FilesAnalyzed
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected FilesAnalyzed to default to true, got false")
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != false {
+ t.Errorf("expected IsFilesAnalyzedTagPresent to default to false, got true")
+ }
+ // and the Document's Packages should still be of size 1 b/c no SPDX
+ // identifier has been seen yet
+ if len(parser.doc.Packages) != 1 {
+ t.Errorf("Expected len(Packages) to be 1, got %d", len(parser.doc.Packages))
+ }
+ if parser.doc.Packages[0] != p1 {
+ t.Errorf("Expected package %v in Packages[package1], got %v", p1, parser.doc.Packages[0])
+ }
+ if parser.doc.Packages[0].PackageName != "package1" {
+ t.Errorf("expected package name %s in Packages[package1], got %s", "package1", parser.doc.Packages[0].PackageName)
+ }
+ // and the first Package's Files should be of size 1 and have f1 only
+ if len(parser.doc.Packages[0].Files) != 1 {
+ t.Errorf("Expected 1 file in Packages[package1].Files, got %d", len(parser.doc.Packages[0].Files))
+ }
+ if parser.doc.Packages[0].Files[0] != f1 {
+ t.Errorf("Expected file %v in Files[f1], got %v", f1, parser.doc.Packages[0].Files[0])
+ }
+ if parser.doc.Packages[0].Files[0].FileName != "f1.txt" {
+ t.Errorf("expected file name %s in Files[f1], got %s", "f1.txt", parser.doc.Packages[0].Files[0].FileName)
+ }
+ // and the new Package should have no files
+ if len(parser.pkg.Files) != 0 {
+ t.Errorf("Expected no files in Packages[1].Files, got %d", len(parser.pkg.Files))
+ }
+ // and the current file should be nil
+ if parser.file != nil {
+ t.Errorf("Expected nil for parser.file, got %v", parser.file)
+ }
+ // and the current snippet should be nil
+ if parser.snippet != nil {
+ t.Errorf("Expected nil for parser.snippet, got %v", parser.snippet)
+ }
+}
+
+func TestParser2_3SnippetMovesToFileAfterParsingFileNameTag(t *testing.T) {
+ f1Name := "f1.txt"
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ p1 := parser.pkg
+ f1 := parser.file
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets["s1"] = parser.snippet
+
+ f2Name := "f2.txt"
+ err := parser.parsePair2_3("FileName", f2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_3 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_3, parser.st)
+ }
+ // and current package should remain what it was
+ if parser.pkg != p1 {
+ t.Fatalf("expected package to remain %v, got %v", p1, parser.pkg)
+ }
+ // and a file should be created
+ if parser.file == nil {
+ t.Fatalf("parser didn't create new file")
+ }
+ // and the file name should be as expected
+ if parser.file.FileName != f2Name {
+ t.Errorf("expected file name %s, got %s", f2Name, parser.file.FileName)
+ }
+ // and the Package's Files should still be of size 1 since we haven't seen
+ // an SPDX identifier yet for this new file
+ if len(parser.pkg.Files) != 1 {
+ t.Errorf("Expected len(Files) to be 1, got %d", len(parser.pkg.Files))
+ }
+ if parser.pkg.Files[0] != f1 {
+ t.Errorf("Expected file %v in Files[f1], got %v", f1, parser.pkg.Files[0])
+ }
+ if parser.pkg.Files[0].FileName != f1Name {
+ t.Errorf("expected file name %s in Files[f1], got %s", f1Name, parser.pkg.Files[0].FileName)
+ }
+ // and the current snippet should be nil
+ if parser.snippet != nil {
+ t.Errorf("Expected nil for parser.snippet, got %v", parser.snippet)
+ }
+}
+
+func TestParser2_3SnippetMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets["s1"] = parser.snippet
+
+ err := parser.parsePair2_3("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_3, parser.st)
+ }
+}
+
+func TestParser2_3SnippetMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets["s1"] = parser.snippet
+
+ err := parser.parsePair2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("expected state to be %v, got %v", psReview2_3, parser.st)
+ }
+}
+
+func TestParser2_3SnippetStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets["s1"] = parser.snippet
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psSnippet2_3 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_3, parser.st)
+ }
+ // and the relationship should be in the Document's Relationships
+ if len(parser.doc.Relationships) != 1 {
+ t.Fatalf("expected doc.Relationships to have len 1, got %d", len(parser.doc.Relationships))
+ }
+ deID := parser.doc.Relationships[0].RefA
+ if deID.DocumentRefID != "" || deID.ElementRefID != "blah" {
+ t.Errorf("expected RefA to be %s, got %s", "blah", parser.doc.Relationships[0].RefA)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psSnippet2_3 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_3, parser.st)
+ }
+}
+
+func TestParser2_3SnippetStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+ parser.file.Snippets["s1"] = parser.snippet
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psSnippet2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psSnippet2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psSnippet2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psSnippet2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psSnippet2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_3)
+ }
+
+ // and the annotation should be in the Document's Annotations
+ if len(parser.doc.Annotations) != 1 {
+ t.Fatalf("expected doc.Annotations to have len 1, got %d", len(parser.doc.Annotations))
+ }
+ if parser.doc.Annotations[0].Annotator.Annotator != "John Doe ()" {
+ t.Errorf("expected Annotator to be %s, got %s", "John Doe ()", parser.doc.Annotations[0].Annotator)
+ }
+}
+
+// ===== Snippet data section tests =====
+func TestParser2_3CanParseSnippetTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ // Snippet SPDX Identifier
+ err := parser.parsePairFromSnippet2_3("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.SnippetSPDXIdentifier != "s1" {
+ t.Errorf("got %v for SnippetSPDXIdentifier", parser.snippet.SnippetSPDXIdentifier)
+ }
+
+ // Snippet from File SPDX Identifier
+ err = parser.parsePairFromSnippet2_3("SnippetFromFileSPDXID", "SPDXRef-f1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ wantDeID := common.DocElementID{DocumentRefID: "", ElementRefID: common.ElementID("f1")}
+ if parser.snippet.SnippetFromFileSPDXIdentifier != wantDeID.ElementRefID {
+ t.Errorf("got %v for SnippetFromFileSPDXIdentifier", parser.snippet.SnippetFromFileSPDXIdentifier)
+ }
+
+ // Snippet Byte Range
+ err = parser.parsePairFromSnippet2_3("SnippetByteRange", "20:320")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.Ranges[0].StartPointer.Offset != 20 {
+ t.Errorf("got %v for SnippetByteRangeStart", parser.snippet.Ranges[0].StartPointer.Offset)
+ }
+ if parser.snippet.Ranges[0].EndPointer.Offset != 320 {
+ t.Errorf("got %v for SnippetByteRangeEnd", parser.snippet.Ranges[0].EndPointer.Offset)
+ }
+
+ // Snippet Line Range
+ err = parser.parsePairFromSnippet2_3("SnippetLineRange", "5:12")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.Ranges[1].StartPointer.LineNumber != 5 {
+ t.Errorf("got %v for SnippetLineRangeStart", parser.snippet.Ranges[1].StartPointer.LineNumber)
+ }
+ if parser.snippet.Ranges[1].EndPointer.LineNumber != 12 {
+ t.Errorf("got %v for SnippetLineRangeEnd", parser.snippet.Ranges[1].EndPointer.LineNumber)
+ }
+
+ // Snippet Concluded License
+ err = parser.parsePairFromSnippet2_3("SnippetLicenseConcluded", "BSD-3-Clause")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.SnippetLicenseConcluded != "BSD-3-Clause" {
+ t.Errorf("got %v for SnippetLicenseConcluded", parser.snippet.SnippetLicenseConcluded)
+ }
+
+ // License Information in Snippet
+ lics := []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "CC0-1.0",
+ }
+ for _, lic := range lics {
+ err = parser.parsePairFromSnippet2_3("LicenseInfoInSnippet", lic)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, licWant := range lics {
+ flagFound := false
+ for _, licCheck := range parser.snippet.LicenseInfoInSnippet {
+ if licWant == licCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in LicenseInfoInSnippet", licWant)
+ }
+ }
+ if len(lics) != len(parser.snippet.LicenseInfoInSnippet) {
+ t.Errorf("expected %d licenses in LicenseInfoInSnippet, got %d", len(lics),
+ len(parser.snippet.LicenseInfoInSnippet))
+ }
+
+ // Snippet Comments on License
+ err = parser.parsePairFromSnippet2_3("SnippetLicenseComments", "this is a comment about the licenses")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.SnippetLicenseComments != "this is a comment about the licenses" {
+ t.Errorf("got %v for SnippetLicenseComments", parser.snippet.SnippetLicenseComments)
+ }
+
+ // Snippet Copyright Text
+ err = parser.parsePairFromSnippet2_3("SnippetCopyrightText", "copyright (c) John Doe and friends")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.SnippetCopyrightText != "copyright (c) John Doe and friends" {
+ t.Errorf("got %v for SnippetCopyrightText", parser.snippet.SnippetCopyrightText)
+ }
+
+ // Snippet Comment
+ err = parser.parsePairFromSnippet2_3("SnippetComment", "this is a comment about the snippet")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.SnippetComment != "this is a comment about the snippet" {
+ t.Errorf("got %v for SnippetComment", parser.snippet.SnippetComment)
+ }
+
+ // Snippet Name
+ err = parser.parsePairFromSnippet2_3("SnippetName", "from some other package called abc")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.snippet.SnippetName != "from some other package called abc" {
+ t.Errorf("got %v for SnippetName", parser.snippet.SnippetName)
+ }
+
+ // Snippet Attribution Texts
+ attrs := []string{
+ "Include this notice in all advertising materials",
+ "This is a \nmulti-line string",
+ }
+ for _, attr := range attrs {
+ err = parser.parsePairFromSnippet2_3("SnippetAttributionText", attr)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ }
+ for _, attrWant := range attrs {
+ flagFound := false
+ for _, attrCheck := range parser.snippet.SnippetAttributionTexts {
+ if attrWant == attrCheck {
+ flagFound = true
+ }
+ }
+ if flagFound == false {
+ t.Errorf("didn't find %s in SnippetAttributionText", attrWant)
+ }
+ }
+ if len(attrs) != len(parser.snippet.SnippetAttributionTexts) {
+ t.Errorf("expected %d attribution texts in SnippetAttributionTexts, got %d", len(attrs),
+ len(parser.snippet.SnippetAttributionTexts))
+ }
+
+}
+
+func TestParser2_3SnippetUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePairFromSnippet2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_3FailsForInvalidSnippetSPDXID(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ // invalid Snippet SPDX Identifier
+ err := parser.parsePairFromSnippet2_3("SnippetSPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsForInvalidSnippetFromFileSPDXID(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ // start with Snippet SPDX Identifier
+ err := parser.parsePairFromSnippet2_3("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid From File identifier
+ err = parser.parsePairFromSnippet2_3("SnippetFromFileSPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsForInvalidSnippetByteValues(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ // start with Snippet SPDX Identifier
+ err := parser.parsePairFromSnippet2_3("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid byte formats and values
+ err = parser.parsePairFromSnippet2_3("SnippetByteRange", "200 210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_3("SnippetByteRange", "a:210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_3("SnippetByteRange", "200:a")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsForInvalidSnippetLineValues(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{Packages: []*v2_3.Package{}},
+ st: psSnippet2_3,
+ pkg: &v2_3.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_3.File{}},
+ file: &v2_3.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_3.Snippet{}},
+ snippet: &v2_3.Snippet{},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ // start with Snippet SPDX Identifier
+ err := parser.parsePairFromSnippet2_3("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid byte formats and values
+ err = parser.parsePairFromSnippet2_3("SnippetLineRange", "200 210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_3("SnippetLineRange", "a:210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_3("SnippetLineRange", "200:a")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FilesWithoutSpdxIdThrowErrorWithSnippets(t *testing.T) {
+ // Invalid file with snippet
+ // Last unpackaged file before the snippet starts
+ // Last file of a package and New package starts
+ fileName := "f2.txt"
+ sid1 := common.ElementID("s1")
+ parser2 := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ file: &v2_3.File{FileName: fileName},
+ }
+ err := parser2.parsePair2_3("SnippetSPDXID", string(sid1))
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+
+}
diff --git a/tvloader/parser2v3/parser.go b/tvloader/parser2v3/parser.go
new file mode 100644
index 0000000..af84d93
--- /dev/null
+++ b/tvloader/parser2v3/parser.go
@@ -0,0 +1,100 @@
+// Package parser2v3 contains functions to read, load and parse
+// SPDX tag-value files, version 2.3.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/tvloader/reader"
+)
+
+// ParseTagValues takes a list of (tag, value) pairs, parses it and returns
+// a pointer to a parsed SPDX Document.
+func ParseTagValues(tvs []reader.TagValuePair) (*v2_3.Document, error) {
+ parser := tvParser2_3{}
+ for _, tv := range tvs {
+ err := parser.parsePair2_3(tv.Tag, tv.Value)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_3 {
+ return nil, fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ if parser.pkg != nil && parser.pkg.PackageSPDXIdentifier == nullSpdxElementId2_3 {
+ return nil, fmt.Errorf("package with PackageName %s does not have SPDX identifier", parser.pkg.PackageName)
+ }
+ return parser.doc, nil
+}
+
+func (parser *tvParser2_3) parsePair2_3(tag string, value string) error {
+ switch parser.st {
+ case psStart2_3:
+ return parser.parsePairFromStart2_3(tag, value)
+ case psCreationInfo2_3:
+ return parser.parsePairFromCreationInfo2_3(tag, value)
+ case psPackage2_3:
+ return parser.parsePairFromPackage2_3(tag, value)
+ case psFile2_3:
+ return parser.parsePairFromFile2_3(tag, value)
+ case psSnippet2_3:
+ return parser.parsePairFromSnippet2_3(tag, value)
+ case psOtherLicense2_3:
+ return parser.parsePairFromOtherLicense2_3(tag, value)
+ case psReview2_3:
+ return parser.parsePairFromReview2_3(tag, value)
+ default:
+ return fmt.Errorf("parser state %v not recognized when parsing (%s, %s)", parser.st, tag, value)
+ }
+}
+
+func (parser *tvParser2_3) parsePairFromStart2_3(tag string, value string) error {
+ // fail if not in Start parser state
+ if parser.st != psStart2_3 {
+ return fmt.Errorf("got invalid state %v in parsePairFromStart2_3", parser.st)
+ }
+
+ // create an SPDX Document data struct if we don't have one already
+ if parser.doc == nil {
+ parser.doc = &v2_3.Document{ExternalDocumentReferences: []v2_3.ExternalDocumentRef{}}
+ }
+
+ switch tag {
+ case "DocumentComment":
+ parser.doc.DocumentComment = value
+ case "SPDXVersion":
+ parser.doc.SPDXVersion = value
+ case "DataLicense":
+ parser.doc.DataLicense = value
+ case "SPDXID":
+ eID, err := extractElementID(value)
+ if err != nil {
+ return err
+ }
+ parser.doc.SPDXIdentifier = eID
+ case "DocumentName":
+ parser.doc.DocumentName = value
+ case "DocumentNamespace":
+ parser.doc.DocumentNamespace = value
+ case "ExternalDocumentRef":
+ documentRefID, uri, alg, checksum, err := extractExternalDocumentReference(value)
+ if err != nil {
+ return err
+ }
+ edr := v2_3.ExternalDocumentRef{
+ DocumentRefID: documentRefID,
+ URI: uri,
+ Checksum: common.Checksum{Algorithm: common.ChecksumAlgorithm(alg), Value: checksum},
+ }
+ parser.doc.ExternalDocumentReferences = append(parser.doc.ExternalDocumentReferences, edr)
+ default:
+ // move to Creation Info parser state
+ parser.st = psCreationInfo2_3
+ return parser.parsePairFromCreationInfo2_3(tag, value)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parser_test.go b/tvloader/parser2v3/parser_test.go
new file mode 100644
index 0000000..dfd8c04
--- /dev/null
+++ b/tvloader/parser2v3/parser_test.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/tvloader/reader"
+)
+
+// ===== Parser exported entry point tests =====
+func TestParser2_3CanParseTagValues(t *testing.T) {
+ var tvPairs []reader.TagValuePair
+
+ // create some pairs
+ tvPair1 := reader.TagValuePair{Tag: "SPDXVersion", Value: "SPDX-2.3"}
+ tvPairs = append(tvPairs, tvPair1)
+ tvPair2 := reader.TagValuePair{Tag: "DataLicense", Value: "CC0-1.0"}
+ tvPairs = append(tvPairs, tvPair2)
+ tvPair3 := reader.TagValuePair{Tag: "SPDXID", Value: "SPDXRef-DOCUMENT"}
+ tvPairs = append(tvPairs, tvPair3)
+
+ // now parse them
+ doc, err := ParseTagValues(tvPairs)
+ if err != nil {
+ t.Errorf("got error when calling ParseTagValues: %v", err)
+ }
+ if doc.SPDXVersion != "SPDX-2.3" {
+ t.Errorf("expected SPDXVersion to be SPDX-2.3, got %v", doc.SPDXVersion)
+ }
+ if doc.DataLicense != "CC0-1.0" {
+ t.Errorf("expected DataLicense to be CC0-1.0, got %v", doc.DataLicense)
+ }
+ if doc.SPDXIdentifier != "DOCUMENT" {
+ t.Errorf("expected SPDXIdentifier to be DOCUMENT, got %v", doc.SPDXIdentifier)
+ }
+
+}
+
+// ===== Parser initialization tests =====
+func TestParser2_3InitCreatesResetStatus(t *testing.T) {
+ parser := tvParser2_3{}
+ if parser.st != psStart2_3 {
+ t.Errorf("parser did not begin in start state")
+ }
+ if parser.doc != nil {
+ t.Errorf("parser did not begin with nil document")
+ }
+}
+
+func TestParser2_3HasDocumentAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_3{}
+ err := parser.parsePair2_3("SPDXVersion", "SPDX-2.3")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.doc == nil {
+ t.Errorf("doc is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_3StartFailsToParseIfInInvalidState(t *testing.T) {
+ parser := tvParser2_3{st: psReview2_3}
+ err := parser.parsePairFromStart2_3("SPDXVersion", "SPDX-2.3")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FilesWithoutSpdxIdThrowErrorAtCompleteParse(t *testing.T) {
+ // case: Checks the last file
+ // Last unpackaged file with no packages in doc
+ // Last file of last package in the doc
+ tvPairs := []reader.TagValuePair{
+ {Tag: "SPDXVersion", Value: "SPDX-2.3"},
+ {Tag: "DataLicense", Value: "CC0-1.0"},
+ {Tag: "SPDXID", Value: "SPDXRef-DOCUMENT"},
+ {Tag: "FileName", Value: "f1"},
+ }
+ _, err := ParseTagValues(tvPairs)
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+}
+
+func TestParser2_3PackageWithoutSpdxIdThrowErrorAtCompleteParse(t *testing.T) {
+ // case: Checks the last package
+ tvPairs := []reader.TagValuePair{
+ {Tag: "SPDXVersion", Value: "SPDX-2.3"},
+ {Tag: "DataLicense", Value: "CC0-1.0"},
+ {Tag: "SPDXID", Value: "SPDXRef-DOCUMENT"},
+ {Tag: "PackageName", Value: "p1"},
+ }
+ _, err := ParseTagValues(tvPairs)
+ if err == nil {
+ t.Errorf("package without SPDX Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v3/types.go b/tvloader/parser2v3/types.go
new file mode 100644
index 0000000..b6dc3ab
--- /dev/null
+++ b/tvloader/parser2v3/types.go
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+type tvParser2_3 struct {
+ // document into which data is being parsed
+ doc *v2_3.Document
+
+ // current parser state
+ st tvParserState2_3
+
+ // current SPDX item being filled in, if any
+ pkg *v2_3.Package
+ pkgExtRef *v2_3.PackageExternalReference
+ file *v2_3.File
+ fileAOP *v2_3.ArtifactOfProject
+ snippet *v2_3.Snippet
+ otherLic *v2_3.OtherLicense
+ rln *v2_3.Relationship
+ ann *v2_3.Annotation
+ rev *v2_3.Review
+ // don't need creation info pointer b/c only one,
+ // and we can get to it via doc.CreationInfo
+}
+
+// parser state (SPDX document version 2.3)
+type tvParserState2_3 int
+
+const (
+ // at beginning of document
+ psStart2_3 tvParserState2_3 = iota
+
+ // in document creation info section
+ psCreationInfo2_3
+
+ // in package data section
+ psPackage2_3
+
+ // in file data section (including "unpackaged" files)
+ psFile2_3
+
+ // in snippet data section (including "unpackaged" files)
+ psSnippet2_3
+
+ // in other license section
+ psOtherLicense2_3
+
+ // in review section
+ psReview2_3
+)
+
+const nullSpdxElementId2_3 = common.ElementID("")
diff --git a/tvloader/parser2v3/util.go b/tvloader/parser2v3/util.go
new file mode 100644
index 0000000..743b3f6
--- /dev/null
+++ b/tvloader/parser2v3/util.go
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// used to extract key / value from embedded substrings
+// returns subkey, subvalue, nil if no error, or "", "", error otherwise
+func extractSubs(value string) (string, string, error) {
+ // parse the value to see if it's a valid subvalue format
+ sp := strings.SplitN(value, ":", 2)
+ if len(sp) == 1 {
+ return "", "", fmt.Errorf("invalid subvalue format for %s (no colon found)", value)
+ }
+
+ subkey := strings.TrimSpace(sp[0])
+ subvalue := strings.TrimSpace(sp[1])
+
+ return subkey, subvalue, nil
+}
+
+// used to extract DocumentRef and SPDXRef values from an SPDX Identifier
+// which can point either to this document or to a different one
+func extractDocElementID(value string) (common.DocElementID, error) {
+ docRefID := ""
+ idStr := value
+
+ // check prefix to see if it's a DocumentRef ID
+ if strings.HasPrefix(idStr, "DocumentRef-") {
+ // extract the part that comes between "DocumentRef-" and ":"
+ strs := strings.Split(idStr, ":")
+ // should be exactly two, part before and part after
+ if len(strs) < 2 {
+ return common.DocElementID{}, fmt.Errorf("no colon found although DocumentRef- prefix present")
+ }
+ if len(strs) > 2 {
+ return common.DocElementID{}, fmt.Errorf("more than one colon found")
+ }
+
+ // trim the prefix and confirm non-empty
+ docRefID = strings.TrimPrefix(strs[0], "DocumentRef-")
+ if docRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("document identifier has nothing after prefix")
+ }
+ // and use remainder for element ID parsing
+ idStr = strs[1]
+ }
+
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(idStr, "SPDXRef-") {
+ return common.DocElementID{}, fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(idStr, ":") {
+ // we know this means there was no DocumentRef- prefix, because
+ // we would have handled multiple colons above if it was
+ return common.DocElementID{}, fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(idStr, "SPDXRef-")
+ if eltRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.DocElementID{DocumentRefID: docRefID, ElementRefID: common.ElementID(eltRefID)}, nil
+}
+
+// used to extract SPDXRef values from an SPDX Identifier, OR "special" strings
+// from a specified set of permitted values. The primary use case for this is
+// the right-hand side of Relationships, where beginning in SPDX 2.3 the values
+// "NONE" and "NOASSERTION" are permitted. If the value does not match one of
+// the specified permitted values, it will fall back to the ordinary
+// DocElementID extractor.
+func extractDocElementSpecial(value string, permittedSpecial []string) (common.DocElementID, error) {
+ // check value against special set first
+ for _, sp := range permittedSpecial {
+ if sp == value {
+ return common.DocElementID{SpecialID: sp}, nil
+ }
+ }
+ // not found, fall back to regular search
+ return extractDocElementID(value)
+}
+
+// used to extract SPDXRef values only from an SPDX Identifier which can point
+// to this document only. Use extractDocElementID for parsing IDs that can
+// refer either to this document or a different one.
+func extractElementID(value string) (common.ElementID, error) {
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(value, "SPDXRef-") {
+ return common.ElementID(""), fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(value, ":") {
+ return common.ElementID(""), fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(value, "SPDXRef-")
+ if eltRefID == "" {
+ return common.ElementID(""), fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.ElementID(eltRefID), nil
+}
diff --git a/tvloader/parser2v3/util_test.go b/tvloader/parser2v3/util_test.go
new file mode 100644
index 0000000..6269b91
--- /dev/null
+++ b/tvloader/parser2v3/util_test.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Helper function tests =====
+
+func TestCanExtractSubvalues(t *testing.T) {
+ subkey, subvalue, err := extractSubs("SHA1: abc123")
+ if err != nil {
+ t.Errorf("got error when calling extractSubs: %v", err)
+ }
+ if subkey != "SHA1" {
+ t.Errorf("got %v for subkey", subkey)
+ }
+ if subvalue != "abc123" {
+ t.Errorf("got %v for subvalue", subvalue)
+ }
+}
+
+func TestReturnsErrorForInvalidSubvalueFormat(t *testing.T) {
+ _, _, err := extractSubs("blah")
+ if err == nil {
+ t.Errorf("expected error when calling extractSubs for invalid format (0 colons), got nil")
+ }
+}
+
+func TestCanExtractDocumentAndElementRefsFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractDocElementID(t, "SPDXRef-file1", false, "", "file1")
+ // test with valid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file2", false, "doc2", "file2")
+ // test with invalid ID in this document
+ helperForExtractDocElementID(t, "a:SPDXRef-file1", true, "", "")
+ helperForExtractDocElementID(t, "file1", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-file1:", true, "", "")
+ // test with invalid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:a", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:SPDXRef-file1", true, "", "")
+ // test with invalid formats
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file1:file2", true, "", "")
+}
+
+func helperForExtractDocElementID(t *testing.T, tst string, wantErr bool, wantDoc string, wantElt string) {
+ deID, err := extractDocElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if deID.DocumentRefID != wantDoc {
+ if wantDoc == "" {
+ t.Errorf("testing %v: want empty string for DocumentRefID, got %v", tst, deID.DocumentRefID)
+ } else {
+ t.Errorf("testing %v: want %v for DocumentRefID, got %v", tst, wantDoc, deID.DocumentRefID)
+ }
+ }
+ if deID.ElementRefID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want empty string for ElementRefID, got %v", tst, deID.ElementRefID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, deID.ElementRefID)
+ }
+ }
+}
+
+func TestCanExtractSpecialDocumentIDs(t *testing.T) {
+ permittedSpecial := []string{"NONE", "NOASSERTION"}
+ // test with valid special values
+ helperForExtractDocElementSpecial(t, permittedSpecial, "NONE", false, "", "", "NONE")
+ helperForExtractDocElementSpecial(t, permittedSpecial, "NOASSERTION", false, "", "", "NOASSERTION")
+ // test with valid regular IDs
+ helperForExtractDocElementSpecial(t, permittedSpecial, "SPDXRef-file1", false, "", "file1", "")
+ helperForExtractDocElementSpecial(t, permittedSpecial, "DocumentRef-doc2:SPDXRef-file2", false, "doc2", "file2", "")
+ helperForExtractDocElementSpecial(t, permittedSpecial, "a:SPDXRef-file1", true, "", "", "")
+ helperForExtractDocElementSpecial(t, permittedSpecial, "DocumentRef-doc2", true, "", "", "")
+ // test with invalid other words not on permitted list
+ helperForExtractDocElementSpecial(t, permittedSpecial, "FOO", true, "", "", "")
+}
+
+func helperForExtractDocElementSpecial(t *testing.T, permittedSpecial []string, tst string, wantErr bool, wantDoc string, wantElt string, wantSpecial string) {
+ deID, err := extractDocElementSpecial(tst, permittedSpecial)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if deID.DocumentRefID != wantDoc {
+ if wantDoc == "" {
+ t.Errorf("testing %v: want empty string for DocumentRefID, got %v", tst, deID.DocumentRefID)
+ } else {
+ t.Errorf("testing %v: want %v for DocumentRefID, got %v", tst, wantDoc, deID.DocumentRefID)
+ }
+ }
+ if deID.ElementRefID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want empty string for ElementRefID, got %v", tst, deID.ElementRefID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, deID.ElementRefID)
+ }
+ }
+ if deID.SpecialID != wantSpecial {
+ if wantSpecial == "" {
+ t.Errorf("testing %v: want empty string for SpecialID, got %v", tst, deID.SpecialID)
+ } else {
+ t.Errorf("testing %v: want %v for SpecialID, got %v", tst, wantSpecial, deID.SpecialID)
+ }
+ }
+}
+
+func TestCanExtractElementRefsOnlyFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractElementID(t, "SPDXRef-file1", false, "file1")
+ // test with valid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-file2", true, "")
+ // test with invalid ID in this document
+ helperForExtractElementID(t, "a:SPDXRef-file1", true, "")
+ helperForExtractElementID(t, "file1", true, "")
+ helperForExtractElementID(t, "SPDXRef-", true, "")
+ helperForExtractElementID(t, "SPDXRef-file1:", true, "")
+ // test with invalid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:a", true, "")
+ helperForExtractElementID(t, "DocumentRef-:", true, "")
+ helperForExtractElementID(t, "DocumentRef-:SPDXRef-file1", true, "")
+}
+
+func helperForExtractElementID(t *testing.T, tst string, wantErr bool, wantElt string) {
+ eID, err := extractElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if eID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, eID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, eID)
+ }
+ }
+}
diff --git a/tvloader/tvloader.go b/tvloader/tvloader.go
index b435f2c..ece2c2b 100644
--- a/tvloader/tvloader.go
+++ b/tvloader/tvloader.go
@@ -4,6 +4,8 @@
package tvloader
import (
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/tvloader/parser2v3"
"io"
"github.com/spdx/tools-golang/spdx/v2_1"
@@ -44,3 +46,19 @@ func Load2_2(content io.Reader) (*v2_2.Document, error) {
return doc, nil
}
+
+// Load2_3 takes an io.Reader and returns a fully-parsed SPDX Document
+// (version 2.2) if parseable, or error if any error is encountered.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ tvPairs, err := reader.ReadTagValues(content)
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := parser2v3.ParseTagValues(tvPairs)
+ if err != nil {
+ return nil, err
+ }
+
+ return doc, nil
+}
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)
+ }
+}
diff --git a/tvsaver/tvsaver.go b/tvsaver/tvsaver.go
index 09d16dc..a73b4b9 100644
--- a/tvsaver/tvsaver.go
+++ b/tvsaver/tvsaver.go
@@ -8,8 +8,10 @@ import (
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
"github.com/spdx/tools-golang/tvsaver/saver2v1"
"github.com/spdx/tools-golang/tvsaver/saver2v2"
+ "github.com/spdx/tools-golang/tvsaver/saver2v3"
)
// Save2_1 takes an io.Writer and an SPDX Document (version 2.1),
@@ -25,3 +27,10 @@ func Save2_1(doc *v2_1.Document, w io.Writer) error {
func Save2_2(doc *v2_2.Document, w io.Writer) error {
return saver2v2.RenderDocument2_2(doc, w)
}
+
+// Save2_3 takes an io.Writer and an SPDX Document (version 2.3),
+// and writes it to the writer in tag-value format. It returns error
+// if any error is encountered.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ return saver2v3.RenderDocument2_3(doc, w)
+}
diff --git a/utils/verification.go b/utils/verification.go
index bd6c875..72523b3 100644
--- a/utils/verification.go
+++ b/utils/verification.go
@@ -12,6 +12,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// GetVerificationCode2_1 takes a slice of files and an optional filename
@@ -91,3 +92,42 @@ func GetVerificationCode2_2(files []*v2_2.File, excludeFile string) (common.Pack
return code, nil
}
+
+// GetVerificationCode2_3 takes a slice of files and an optional filename
+// for an "excludes" file, and returns a Package Verification Code calculated
+// according to SPDX spec version 2.3, section 3.9.4.
+func GetVerificationCode2_3(files []*v2_3.File, excludeFile string) (common.PackageVerificationCode, error) {
+ // create slice of strings - unsorted SHA1s for all files
+ shas := []string{}
+ for i, f := range files {
+ if f == nil {
+ return common.PackageVerificationCode{}, fmt.Errorf("got nil file for identifier %v", i)
+ }
+ if f.FileName != excludeFile {
+ // find the SHA1 hash, if present
+ for _, checksum := range f.Checksums {
+ if checksum.Algorithm == common.SHA1 {
+ shas = append(shas, checksum.Value)
+ }
+ }
+ }
+ }
+
+ // sort the strings
+ sort.Strings(shas)
+
+ // concatenate them into one string, with no trailing separators
+ shasConcat := strings.Join(shas, "")
+
+ // and get its SHA1 value
+ hsha1 := sha1.New()
+ hsha1.Write([]byte(shasConcat))
+ bs := hsha1.Sum(nil)
+
+ code := common.PackageVerificationCode{
+ Value: fmt.Sprintf("%x", bs),
+ ExcludedFiles: []string{excludeFile},
+ }
+
+ return code, nil
+}
diff --git a/utils/verification_test.go b/utils/verification_test.go
index 3fa4ead..beee8b6 100644
--- a/utils/verification_test.go
+++ b/utils/verification_test.go
@@ -8,6 +8,7 @@ import (
"github.com/spdx/tools-golang/spdx/common"
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
)
// ===== 2.1 Verification code functionality tests =====
@@ -277,3 +278,167 @@ func TestPackage2_2GetVerificationCodeFailsIfNilFileInSlice(t *testing.T) {
t.Fatalf("expected non-nil error, got nil")
}
}
+
+// ===== 2.3 Verification code functionality tests =====
+
+func TestPackage2_3CanGetVerificationCode(t *testing.T) {
+ files := []*v2_3.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "3333333333bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file5.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "2222222222bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa",
+ },
+ },
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "ac924b375119c81c1f08c3e2722044bfbbdcd3dc"}
+
+ gotCode, err := GetVerificationCode2_3(files, "")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+
+}
+
+func TestPackage2_3CanGetVerificationCodeIgnoringExcludesFile(t *testing.T) {
+ files := []*v2_3.File{
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "3333333333bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "thisfile.spdx",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa",
+ },
+ },
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "2222222222bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "17fab1bd18fe5c13b5d3983f1c17e5f88b8ff266"}
+
+ gotCode, err := GetVerificationCode2_3(files, "thisfile.spdx")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+}
+
+func TestPackage2_3GetVerificationCodeFailsIfNilFileInSlice(t *testing.T) {
+ files := []*v2_3.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ nil,
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ }
+
+ _, err := GetVerificationCode2_3(files, "")
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/yaml/parser.go b/yaml/parser.go
index 1a8f0f6..265a04d 100644
--- a/yaml/parser.go
+++ b/yaml/parser.go
@@ -7,6 +7,7 @@ import (
"io"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
"sigs.k8s.io/yaml"
)
@@ -27,3 +28,21 @@ func Load2_2(content io.Reader) (*v2_2.Document, error) {
return &doc, nil
}
+
+// Load2_3 takes in an io.Reader and returns an SPDX document.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ // convert io.Reader to a slice of bytes and call the parser
+ buf := new(bytes.Buffer)
+ _, err := buf.ReadFrom(content)
+ if err != nil {
+ return nil, err
+ }
+
+ var doc v2_3.Document
+ err = yaml.Unmarshal(buf.Bytes(), &doc)
+ if err != nil {
+ return nil, err
+ }
+
+ return &doc, nil
+}
diff --git a/yaml/writer.go b/yaml/writer.go
index cbc6508..06ef69a 100644
--- a/yaml/writer.go
+++ b/yaml/writer.go
@@ -6,6 +6,7 @@ import (
"io"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
"sigs.k8s.io/yaml"
)
@@ -23,3 +24,18 @@ func Save2_2(doc *v2_2.Document, w io.Writer) error {
return nil
}
+
+// Save2_3 takes an SPDX Document (version 2.3) and an io.Writer, and writes the document to the writer in YAML format.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ buf, err := yaml.Marshal(doc)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/yaml/yaml_test.go b/yaml/yaml_v2_2_test.go
index c38b0e1..9d66f5b 100644
--- a/yaml/yaml_test.go
+++ b/yaml/yaml_v2_2_test.go
@@ -27,7 +27,7 @@ func TestLoad2_2(t *testing.T) {
}
// get a copy of the handwritten struct so we don't mutate it on accident
- handwrittenExample := want
+ handwrittenExample := want2_2
if cmp.Equal(handwrittenExample, got) {
t.Errorf("got incorrect struct after parsing YAML example")
@@ -38,7 +38,7 @@ func TestLoad2_2(t *testing.T) {
func TestWrite2_2(t *testing.T) {
w := &bytes.Buffer{}
// get a copy of the handwritten struct so we don't mutate it on accident
- handwrittenExample := want
+ handwrittenExample := want2_2
if err := Save2_2(&handwrittenExample, w); err != nil {
t.Errorf("Save2_2() error = %v", err.Error())
return
@@ -60,7 +60,7 @@ func TestWrite2_2(t *testing.T) {
// want is handwritten translation of the official example YAML SPDX v2.2 document into a Go struct.
// We expect that the result of parsing the official document should be this value.
// We expect that the result of writing this struct should match the official example document.
-var want = v2_2.Document{
+var want2_2 = v2_2.Document{
DataLicense: "CC0-1.0",
SPDXVersion: "SPDX-2.2",
SPDXIdentifier: "SPDXRef-DOCUMENT",
diff --git a/yaml/yaml_v2_3_test.go b/yaml/yaml_v2_3_test.go
new file mode 100644
index 0000000..0271150
--- /dev/null
+++ b/yaml/yaml_v2_3_test.go
@@ -0,0 +1,492 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_yaml
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+var update = *flag.Bool("update-snapshots", false, "update the example snapshot")
+
+func TestLoad2_3(t *testing.T) {
+ fileName := "../examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml"
+
+ if update {
+ f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ t.Errorf("unable to write SPDX 2.3 example to YAML: %v", err)
+ }
+ err = Save2_3(&want2_3, f)
+ if err != nil {
+ t.Errorf("unable to serialize SPDX 2.3 example to YAML: %v", err)
+ }
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := Load2_3(file)
+ if err != nil {
+ t.Errorf("yaml.parser.Load2_3() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("got incorrect struct after parsing YAML example")
+ return
+ }
+}
+
+func TestWrite2_3(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+ if err := Save2_3(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_3() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original handwritten struct
+ parsedDoc, err := Load2_3(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("got incorrect struct after writing and re-parsing YAML example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example YAML SPDX v2.2 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want2_3 = v2_3.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.2",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {CreatorType: "Tool", Creator: "LicenseFind-1.0"},
+ {CreatorType: "Organization", Creator: "ExampleCodeInspect ()"},
+ {CreatorType: "Person", Creator: "Jane Doe ()"},
+ },
+ Created: "2010-01-29T18:30:22Z",
+ CreatorComment: "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ },
+ DocumentComment: "This document was created using SPDX 2.0 using licenses from the web site.",
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{
+ {
+ DocumentRefID: "DocumentRef-spdx-tool-1.2",
+ URI: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ Checksum: common.Checksum{
+ Algorithm: common.SHA1,
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2759",
+ },
+ },
+ },
+ OtherLicenses: []*v2_3.OtherLicense{
+ {
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-4",
+ ExtractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-Beerware-4.2",
+ ExtractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ LicenseComment: "The beerware license has a couple of other standard variants.",
+ LicenseName: "Beer-Ware License (Version 42)",
+ LicenseCrossReferences: []string{"http://people.freebsd.org/~phk/"},
+ },
+ {
+ LicenseIdentifier: "LicenseRef-3",
+ ExtractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ LicenseName: "CyberNeko License",
+ LicenseCrossReferences: []string{
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com",
+ },
+ LicenseComment: "This is tye CyperNeko License",
+ },
+ },
+ Annotations: []*v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Jane Doe ()",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Document level annotation",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Joe Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-02-10T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Suzanne Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-03-13T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "Another example reviewer.",
+ },
+ },
+ Packages: []*v2_3.Package{
+ {
+ PackageName: "glibc",
+ PackageSPDXIdentifier: "SPDXRef-Package",
+ PackageVersion: "2.11.1",
+ PackageFileName: "glibc-2.11.1.tar.gz",
+ PackageSupplier: &common.Supplier{
+ Supplier: "Jane Doe (jane.doe@example.com)",
+ SupplierType: "Person",
+ },
+ PackageOriginator: &common.Originator{
+ Originator: "ExampleCodeInspect (contact@example.com)",
+ OriginatorType: "Organization",
+ },
+ PackageDownloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ FilesAnalyzed: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ ExcludedFiles: []string{"./package.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: "SHA256",
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ },
+ PackageHomePage: "http://ftp.gnu.org/gnu/glibc",
+ PackageSourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ PackageLicenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)",
+ PackageLicenseInfoFromFiles: []string{
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1",
+ },
+ PackageLicenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)",
+ PackageLicenseComments: "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ PackageCopyrightText: "Copyright 2008-2010 John Smith",
+ PackageSummary: "GNU C library.",
+ PackageDescription: "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ PackageComment: "",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "SECURITY",
+ RefType: "cpe23Type",
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ },
+ {
+ Category: "OTHER",
+ RefType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ Locator: "acmecorp/acmenator/4.1.3-alpha",
+ ExternalRefComment: "This is the external ref for Acme",
+ },
+ },
+ PackageAttributionTexts: []string{
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.",
+ },
+ Files: nil,
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "PACKAGE_MANAGER",
+ RefType: "purl",
+ Locator: "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ },
+ },
+ FilesAnalyzed: false,
+ PackageHomePage: "http://www.openjena.org/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageVersion: "3.12.0",
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-Saxon",
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ PackageCopyrightText: "Copyright Saxonica Ltd",
+ PackageDescription: "The Saxon package is a collection of tools for processing XML documents.",
+ PackageDownloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://saxon.sourceforge.net/",
+ PackageLicenseComments: "Other versions available for a commercial license",
+ PackageLicenseConcluded: "MPL-1.0",
+ PackageLicenseDeclared: "MPL-1.0",
+ PackageName: "Saxon",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "8.8",
+ },
+ {
+ PrimaryPackagePurpose: "CONTAINER",
+ PackageSPDXIdentifier: "SPDXRef-CentOS-7",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDescription: "The CentOS container used to run the application.",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "https://www.centos.org/",
+ PackageName: "centos",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "centos7.9.2009",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ },
+ },
+ Files: []*v2_3.File{
+ {
+ FileName: "./src/org/spdx/parser/DOAPProject.java",
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ FileTypes: []string{
+ "SOURCE",
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright 2010, 2011 Source Auditor Inc.",
+ FileContributors: []string{
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c",
+ },
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-CommonsLangSrc",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file is used by Jena",
+ FileCopyrightText: "Copyright 2001-2011 The Apache Software Foundation",
+ FileContributors: []string{"Apache Software Foundation"},
+ FileName: "./lib-source/commons-lang3-3.1-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileNotice: "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-JenaLib",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file belongs to Jena",
+ FileCopyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ FileContributors: []string{"Apache Software Foundation", "Hewlett Packard Inc."},
+ FileName: "./lib-source/jena-2.6.3-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseComments: "This license is used by Jena",
+ LicenseConcluded: "LicenseRef-1",
+ LicenseInfoInFiles: []string{"LicenseRef-1"},
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-File",
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "File Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "File level annotation",
+ },
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ },
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ FileComment: "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ FileCopyrightText: "Copyright 2008-2010 John Smith",
+ FileContributors: []string{"The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"},
+ FileName: "./package/foo.c",
+ FileTypes: []string{"SOURCE"},
+ LicenseComments: "The concluded license was taken from the package level that the file was included in.",
+ LicenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)",
+ LicenseInfoInFiles: []string{"GPL-2.0-only", "LicenseRef-2"},
+ FileNotice: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ },
+ },
+ Snippets: []v2_3.Snippet{
+ {
+ SnippetSPDXIdentifier: "SPDXRef-Snippet",
+ SnippetFromFileSPDXIdentifier: "SPDXRef-DoapSource",
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{
+ Offset: 310,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ Offset: 420,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ {
+ StartPointer: common.SnippetRangePointer{
+ LineNumber: 5,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ LineNumber: 23,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-only",
+ LicenseInfoInSnippet: []string{"GPL-2.0-only"},
+ SnippetLicenseComments: "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ SnippetCopyrightText: "Copyright 2008-2010 John Smith",
+ SnippetComment: "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ SnippetName: "from linux kernel",
+ SnippetAttributionTexts: []string{"Snippet attribution"},
+ },
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ RelationshipComment: "A relationship comment",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("spdx-tool-1.2", "ToolsElement"),
+ Relationship: "COPY_OF",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "JenaLib"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "Saxon"),
+ Relationship: "DYNAMIC_LINK",
+ },
+ {
+ RefA: common.MakeDocElementID("", "CommonsLangSrc"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "GENERATED_FROM",
+ },
+ {
+ RefA: common.MakeDocElementID("", "JenaLib"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "File"),
+ RefB: common.MakeDocElementID("", "fromDoap-0"),
+ Relationship: "GENERATED_FROM",
+ },
+ },
+ Reviews: []*v2_3.Review{
+ {
+ Reviewer: "joe@example.com",
+ ReviewerType: "Person",
+ ReviewDate: "2021-11-03T05:43:21Z",
+ ReviewComment: "This is a review comment",
+ },
+ },
+}