From be2eb824595afe28c87c9a33c2da565480d1eb54 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Fri, 7 Oct 2022 15:29:25 -0400 Subject: chore: add tag value reader and parser for v2.3 Signed-off-by: Keith Zantow --- tvloader/parser2v3/parse_annotation.go | 43 + tvloader/parser2v3/parse_annotation_test.go | 158 ++++ tvloader/parser2v3/parse_creation_info.go | 156 ++++ tvloader/parser2v3/parse_creation_info_test.go | 427 +++++++++ tvloader/parser2v3/parse_file.go | 141 +++ tvloader/parser2v3/parse_file_test.go | 965 ++++++++++++++++++++ tvloader/parser2v3/parse_other_license.go | 55 ++ tvloader/parser2v3/parse_other_license_test.go | 339 +++++++ tvloader/parser2v3/parse_package.go | 232 +++++ tvloader/parser2v3/parse_package_test.go | 1130 ++++++++++++++++++++++++ tvloader/parser2v3/parse_relationship.go | 54 ++ tvloader/parser2v3/parse_relationship_test.go | 202 +++++ tvloader/parser2v3/parse_review.go | 63 ++ tvloader/parser2v3/parse_review_test.go | 414 +++++++++ tvloader/parser2v3/parse_snippet.go | 137 +++ tvloader/parser2v3/parse_snippet_test.go | 635 +++++++++++++ tvloader/parser2v3/parser.go | 100 +++ tvloader/parser2v3/parser_test.go | 97 ++ tvloader/parser2v3/types.go | 57 ++ tvloader/parser2v3/util.go | 115 +++ tvloader/parser2v3/util_test.go | 156 ++++ tvloader/tvloader.go | 18 + 22 files changed, 5694 insertions(+) create mode 100644 tvloader/parser2v3/parse_annotation.go create mode 100644 tvloader/parser2v3/parse_annotation_test.go create mode 100644 tvloader/parser2v3/parse_creation_info.go create mode 100644 tvloader/parser2v3/parse_creation_info_test.go create mode 100644 tvloader/parser2v3/parse_file.go create mode 100644 tvloader/parser2v3/parse_file_test.go create mode 100644 tvloader/parser2v3/parse_other_license.go create mode 100644 tvloader/parser2v3/parse_other_license_test.go create mode 100644 tvloader/parser2v3/parse_package.go create mode 100644 tvloader/parser2v3/parse_package_test.go create mode 100644 tvloader/parser2v3/parse_relationship.go create mode 100644 tvloader/parser2v3/parse_relationship_test.go create mode 100644 tvloader/parser2v3/parse_review.go create mode 100644 tvloader/parser2v3/parse_review_test.go create mode 100644 tvloader/parser2v3/parse_snippet.go create mode 100644 tvloader/parser2v3/parse_snippet_test.go create mode 100644 tvloader/parser2v3/parser.go create mode 100644 tvloader/parser2v3/parser_test.go create mode 100644 tvloader/parser2v3/types.go create mode 100644 tvloader/parser2v3/util.go create mode 100644 tvloader/parser2v3/util_test.go (limited to 'tvloader') 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..4f0398a --- /dev/null +++ b/tvloader/parser2v3/parse_file.go @@ -0,0 +1,141 @@ +// 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: + 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 +} -- cgit v1.2.3