aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--examples/sample-docs/tv/SPDXTagExample-v2.3.spdx328
-rw-r--r--spdx/v2_3/file.go2
-rw-r--r--test/v2_3/tv_test.go491
-rw-r--r--tvloader/parser2v3/parse_annotation.go43
-rw-r--r--tvloader/parser2v3/parse_annotation_test.go158
-rw-r--r--tvloader/parser2v3/parse_creation_info.go156
-rw-r--r--tvloader/parser2v3/parse_creation_info_test.go427
-rw-r--r--tvloader/parser2v3/parse_file.go141
-rw-r--r--tvloader/parser2v3/parse_file_test.go965
-rw-r--r--tvloader/parser2v3/parse_other_license.go55
-rw-r--r--tvloader/parser2v3/parse_other_license_test.go339
-rw-r--r--tvloader/parser2v3/parse_package.go232
-rw-r--r--tvloader/parser2v3/parse_package_test.go1130
-rw-r--r--tvloader/parser2v3/parse_relationship.go54
-rw-r--r--tvloader/parser2v3/parse_relationship_test.go202
-rw-r--r--tvloader/parser2v3/parse_review.go63
-rw-r--r--tvloader/parser2v3/parse_review_test.go414
-rw-r--r--tvloader/parser2v3/parse_snippet.go137
-rw-r--r--tvloader/parser2v3/parse_snippet_test.go635
-rw-r--r--tvloader/parser2v3/parser.go100
-rw-r--r--tvloader/parser2v3/parser_test.go97
-rw-r--r--tvloader/parser2v3/types.go57
-rw-r--r--tvloader/parser2v3/util.go115
-rw-r--r--tvloader/parser2v3/util_test.go156
-rw-r--r--tvloader/tvloader.go18
-rw-r--r--tvsaver/saver2v3/save_annotation.go32
-rw-r--r--tvsaver/saver2v3/save_annotation_test.go110
-rw-r--r--tvsaver/saver2v3/save_creation_info.go30
-rw-r--r--tvsaver/saver2v3/save_creation_info_test.go112
-rw-r--r--tvsaver/saver2v3/save_document.go104
-rw-r--r--tvsaver/saver2v3/save_document_test.go343
-rw-r--r--tvsaver/saver2v3/save_file.go81
-rw-r--r--tvsaver/saver2v3/save_file_test.go314
-rw-r--r--tvsaver/saver2v3/save_other_license.go32
-rw-r--r--tvsaver/saver2v3/save_other_license_test.go83
-rw-r--r--tvsaver/saver2v3/save_package.go129
-rw-r--r--tvsaver/saver2v3/save_package_test.go531
-rw-r--r--tvsaver/saver2v3/save_relationship.go24
-rw-r--r--tvsaver/saver2v3/save_relationship_test.go145
-rw-r--r--tvsaver/saver2v3/save_review.go26
-rw-r--r--tvsaver/saver2v3/save_review_test.go98
-rw-r--r--tvsaver/saver2v3/save_snippet.go55
-rw-r--r--tvsaver/saver2v3/save_snippet_test.go144
-rw-r--r--tvsaver/saver2v3/util.go16
-rw-r--r--tvsaver/saver2v3/util_test.go32
-rw-r--r--tvsaver/tvsaver.go9
46 files changed, 8964 insertions, 1 deletions
diff --git a/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx b/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx
new file mode 100644
index 0000000..7c5ae3d
--- /dev/null
+++ b/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx
@@ -0,0 +1,328 @@
+SPDXVersion: SPDX-2.3
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-SPDXRef-DOCUMENT
+DocumentName: SPDX-Tools-v2.0
+DocumentNamespace: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
+ExternalDocumentRef: DocumentRef-DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759
+DocumentComment: This document was created using SPDX 2.0 using licenses from the web site.
+LicenseListVersion: 3.9
+Creator: Tool: LicenseFind-1.0
+Creator: Organization: ExampleCodeInspect ()
+Creator: Person: Jane Doe ()
+Created: 2010-01-29T18:30:22Z
+CreatorComment: <text>This package has been shipped in source and binary form.
+The binaries were created with gcc 4.5.1 and expect to link to
+compatible system run time libraries.</text>
+
+##### Unpackaged files
+
+FileName: ./lib-source/commons-lang3-3.1-sources.jar
+SPDXID: SPDXRef-SPDXRef-CommonsLangSrc
+FileType: ARCHIVE
+FileChecksum: SHA1: c2b4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright 2001-2011 The Apache Software Foundation
+FileComment: This file is used by Jena
+FileNotice: <text>Apache Commons Lang
+Copyright 2001-2011 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software from the Spring Framework,
+under the Apache License 2.0 (see: StringUtils.containsWhitespace())</text>
+FileContributor: Apache Software Foundation
+
+FileName: ./src/org/spdx/parser/DOAPProject.java
+SPDXID: SPDXRef-SPDXRef-DoapSource
+FileType: SOURCE
+FileChecksum: SHA1: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright 2010, 2011 Source Auditor Inc.
+FileContributor: Protecode Inc.
+FileContributor: SPDX Technical Team Members
+FileContributor: Open Logic Inc.
+FileContributor: Source Auditor Inc.
+FileContributor: Black Duck Software In.c
+
+FileName: ./package/foo.c
+SPDXID: SPDXRef-SPDXRef-File
+FileType: SOURCE
+FileChecksum: SHA1: d6a770ba38583ed4bb4525bd96e50461655d2758
+FileChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+LicenseConcluded: (LGPL-2.0-only OR LicenseRef-2)
+LicenseInfoInFile: GPL-2.0-only
+LicenseInfoInFile: LicenseRef-2
+LicenseComments: The concluded license was taken from the package level that the file was included in.
+FileCopyrightText: Copyright 2008-2010 John Smith
+FileComment: <text>The concluded license was taken from the package level that the file was included in.
+This information was found in the COPYING.txt file in the xyz directory.</text>
+FileNotice: <text>Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</text>
+FileContributor: The Regents of the University of California
+FileContributor: Modified by Paul Mundt lethal@linux-sh.org
+FileContributor: IBM Corporation
+
+FileName: ./lib-source/jena-2.6.3-sources.jar
+SPDXID: SPDXRef-SPDXRef-JenaLib
+FileType: ARCHIVE
+FileChecksum: SHA1: 3ab4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: LicenseRef-1
+LicenseInfoInFile: LicenseRef-1
+LicenseComments: This license is used by Jena
+FileCopyrightText: (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+FileComment: This file belongs to Jena
+FileContributor: Apache Software Foundation
+FileContributor: Hewlett Packard Inc.
+
+##### Package: centos
+
+PackageName: centos
+SPDXID: SPDXRef-SPDXRef-CentOS-7
+PackageVersion: centos7.9.2009
+PackageFileName: saxonB-8.8.zip
+PackageDownloadLocation: NOASSERTION
+PrimaryPackagePurpose: CONTAINER
+ReleaseDate: 2021-10-15T02:38:00Z
+BuiltDate: 2021-09-15T02:38:00Z
+ValidUntilDate: 2022-10-15T02:38:00Z
+FilesAnalyzed: false
+PackageHomePage: https://www.centos.org/
+PackageCopyrightText: NOASSERTION
+PackageDescription: The CentOS container used to run the application.
+
+##### Package: glibc
+
+PackageName: glibc
+SPDXID: SPDXRef-SPDXRef-Package
+PackageVersion: 2.11.1
+PackageFileName: glibc-2.11.1.tar.gz
+PackageSupplier: Person: Jane Doe (jane.doe@example.com)
+PackageOriginator: Organization: ExampleCodeInspect (contact@example.com)
+PackageDownloadLocation: http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz
+PackageVerificationCode: d6a770ba38583ed4bb4525bd96e50461655d2758 (excludes: ./package.spdx)
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageHomePage: http://ftp.gnu.org/gnu/glibc
+PackageSourceInfo: uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.
+PackageLicenseConcluded: (LGPL-2.0-only OR LicenseRef-3)
+PackageLicenseInfoFromFiles: GPL-2.0-only
+PackageLicenseInfoFromFiles: LicenseRef-2
+PackageLicenseInfoFromFiles: LicenseRef-1
+PackageLicenseDeclared: (LGPL-2.0-only AND LicenseRef-3)
+PackageLicenseComments: The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.
+PackageCopyrightText: Copyright 2008-2010 John Smith
+PackageSummary: GNU C library.
+PackageDescription: The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.
+ExternalRef: SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*
+ExternalRef: OTHER http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge acmecorp/acmenator/4.1.3-alpha
+ExternalRefComment: This is the external ref for Acme
+PackageAttributionText: The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.
+
+##### Package: Saxon
+
+PackageName: Saxon
+SPDXID: SPDXRef-SPDXRef-Saxon
+PackageVersion: 8.8
+PackageFileName: saxonB-8.8.zip
+PackageDownloadLocation: https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download
+FilesAnalyzed: false
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageHomePage: http://saxon.sourceforge.net/
+PackageLicenseConcluded: MPL-1.0
+PackageLicenseDeclared: MPL-1.0
+PackageLicenseComments: Other versions available for a commercial license
+PackageCopyrightText: Copyright Saxonica Ltd
+PackageDescription: The Saxon package is a collection of tools for processing XML documents.
+
+##### Package: Jena
+
+PackageName: Jena
+SPDXID: SPDXRef-SPDXRef-fromDoap-0
+PackageVersion: 3.12.0
+PackageDownloadLocation: https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz
+FilesAnalyzed: false
+PackageHomePage: http://www.openjena.org/
+PackageLicenseConcluded: NOASSERTION
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: NOASSERTION
+ExternalRef: PACKAGE-MANAGER purl pkg:maven/org.apache.jena/apache-jena@3.12.0
+
+##### Package: Apache Commons Lang
+
+PackageName: Apache Commons Lang
+SPDXID: SPDXRef-SPDXRef-fromDoap-1
+PackageDownloadLocation: NOASSERTION
+FilesAnalyzed: false
+PackageHomePage: http://commons.apache.org/proper/commons-lang/
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: NOASSERTION
+
+##### Other Licenses
+
+LicenseID: LicenseRef-1
+ExtractedText: <text>/*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</text>
+
+LicenseID: LicenseRef-2
+ExtractedText: <text>This package includes the GRDDL parser developed by Hewlett Packard under the following license:
+� Copyright 2007 Hewlett-Packard Development Company, LP
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</text>
+
+LicenseID: LicenseRef-4
+ExtractedText: <text>/*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</text>
+
+LicenseID: LicenseRef-Beerware-4.2
+ExtractedText: <text>"THE BEER-WARE LICENSE" (Revision 42):
+phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp</text>
+LicenseName: Beer-Ware License (Version 42)
+LicenseCrossReference: http://people.freebsd.org/~phk/
+LicenseComment: The beerware license has a couple of other standard variants.
+
+LicenseID: LicenseRef-3
+ExtractedText: <text>The CyberNeko Software License, Version 1.0
+
+
+(C) Copyright 2002-2005, Andy Clark. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. The end-user documentation included with the redistribution,
+ if any, must include the following acknowledgment:
+ "This product includes software developed by Andy Clark."
+ Alternately, this acknowledgment may appear in the software itself,
+ if and wherever such third-party acknowledgments normally appear.
+
+4. The names "CyberNeko" and "NekoHTML" must not be used to endorse
+ or promote products derived from this software without prior
+ written permission. For written permission, please contact
+ andyc@cyberneko.net.
+
+5. Products derived from this software may not be called "CyberNeko",
+ nor may "CyberNeko" appear in their name, without prior written
+ permission of the author.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</text>
+LicenseName: CyberNeko License
+LicenseCrossReference: http://people.apache.org/~andyc/neko/LICENSE
+LicenseCrossReference: http://justasample.url.com
+LicenseComment: This is tye CyperNeko License
+
+##### Relationships
+
+Relationship: SPDXRef-DOCUMENT CONTAINS SPDXRef-Package
+Relationship: SPDXRef-DOCUMENT COPY_OF DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package
+Relationship: SPDXRef-Package CONTAINS SPDXRef-JenaLib
+Relationship: SPDXRef-Package DYNAMIC_LINK SPDXRef-Saxon
+Relationship: SPDXRef-CommonsLangSrc GENERATED_FROM NOASSERTION
+Relationship: SPDXRef-JenaLib CONTAINS SPDXRef-Package
+Relationship: SPDXRef-File GENERATED_FROM SPDXRef-fromDoap-0
+
+##### Annotations
+
+Annotator: Person: Jane Doe ()
+AnnotationDate: 2010-01-29T18:30:22Z
+AnnotationType: OTHER
+AnnotationComment: Document level annotation
+
+Annotator: Person: Joe Reviewer
+AnnotationDate: 2010-02-10T00:00:00Z
+AnnotationType: REVIEW
+AnnotationComment: This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses
+
+Annotator: Person: Suzanne Reviewer
+AnnotationDate: 2011-03-13T00:00:00Z
+AnnotationType: REVIEW
+AnnotationComment: Another example reviewer.
+
+##### Reviews
+
+Reviewer: Person: joe@example.com
+ReviewDate: 2021-11-03T05:43:21Z
+ReviewComment: This is a review comment
+
diff --git a/spdx/v2_3/file.go b/spdx/v2_3/file.go
index 1dd59fa..c472fdb 100644
--- a/spdx/v2_3/file.go
+++ b/spdx/v2_3/file.go
@@ -41,7 +41,7 @@ type File struct {
// DEPRECATED in version 2.1 of spec
// 8.9-8.11: Artifact of Project variables (defined below)
// Cardinality: optional, one or many
- ArtifactOfProjects []ArtifactOfProject `json:"artifactOfs,omitempty"`
+ ArtifactOfProjects []*ArtifactOfProject `json:"artifactOfs,omitempty"`
// 8.12: File Comment
// Cardinality: optional, one
diff --git a/test/v2_3/tv_test.go b/test/v2_3/tv_test.go
new file mode 100644
index 0000000..9be0459
--- /dev/null
+++ b/test/v2_3/tv_test.go
@@ -0,0 +1,491 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/tvloader"
+ "github.com/spdx/tools-golang/tvsaver"
+)
+
+var update = *flag.Bool("update-snapshots", false, "update the example snapshot")
+
+func TestLoad(t *testing.T) {
+ fileName := "../../examples/sample-docs/tv/SPDXTagExample-v2.3.spdx"
+
+ if update {
+ f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ t.Errorf("unable to open file to write SPDX 2.3 example: %v", err)
+ }
+ err = tvsaver.Save2_3(&want, f)
+ if err != nil {
+ t.Errorf("unable to save SPDX 2.3 example: %v", err)
+ }
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := tvloader.Load2_3(file)
+ if err != nil {
+ t.Errorf("Load2_3() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("Got incorrect struct after parsing example")
+ return
+ }
+}
+
+func TestWrite(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want
+ if err := tvsaver.Save2_3(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_3() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original struct we wrote
+ parsedDoc, err := tvloader.Load2_3(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("Got incorrect struct after writing and re-parsing example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example SPDX v2.3 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want = v2_3.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.3",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {CreatorType: "Tool", Creator: "LicenseFind-1.0"},
+ {CreatorType: "Organization", Creator: "ExampleCodeInspect ()"},
+ {CreatorType: "Person", Creator: "Jane Doe ()"},
+ },
+ Created: "2010-01-29T18:30:22Z",
+ CreatorComment: "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ },
+ DocumentComment: "This document was created using SPDX 2.0 using licenses from the web site.",
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{
+ {
+ DocumentRefID: "DocumentRef-spdx-tool-1.2",
+ URI: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ Checksum: common.Checksum{
+ Algorithm: common.SHA1,
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2759",
+ },
+ },
+ },
+ OtherLicenses: []*v2_3.OtherLicense{
+ {
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-4",
+ ExtractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-Beerware-4.2",
+ ExtractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ LicenseComment: "The beerware license has a couple of other standard variants.",
+ LicenseName: "Beer-Ware License (Version 42)",
+ LicenseCrossReferences: []string{"http://people.freebsd.org/~phk/"},
+ },
+ {
+ LicenseIdentifier: "LicenseRef-3",
+ ExtractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ LicenseName: "CyberNeko License",
+ LicenseCrossReferences: []string{
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com",
+ },
+ LicenseComment: "This is tye CyperNeko License",
+ },
+ },
+ Annotations: []*v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Jane Doe ()",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Document level annotation",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Joe Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-02-10T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Suzanne Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-03-13T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "Another example reviewer.",
+ },
+ },
+ Packages: []*v2_3.Package{
+ {
+ PackageName: "glibc",
+ PackageSPDXIdentifier: "SPDXRef-Package",
+ PackageVersion: "2.11.1",
+ PackageFileName: "glibc-2.11.1.tar.gz",
+ PackageSupplier: &common.Supplier{
+ Supplier: "Jane Doe (jane.doe@example.com)",
+ SupplierType: "Person",
+ },
+ PackageOriginator: &common.Originator{
+ Originator: "ExampleCodeInspect (contact@example.com)",
+ OriginatorType: "Organization",
+ },
+ PackageDownloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ FilesAnalyzed: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ ExcludedFiles: []string{"./package.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: "SHA256",
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ },
+ PackageHomePage: "http://ftp.gnu.org/gnu/glibc",
+ PackageSourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ PackageLicenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)",
+ PackageLicenseInfoFromFiles: []string{
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1",
+ },
+ PackageLicenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)",
+ PackageLicenseComments: "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ PackageCopyrightText: "Copyright 2008-2010 John Smith",
+ PackageSummary: "GNU C library.",
+ PackageDescription: "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ PackageComment: "",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "SECURITY",
+ RefType: "cpe23Type",
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ },
+ {
+ Category: "OTHER",
+ RefType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ Locator: "acmecorp/acmenator/4.1.3-alpha",
+ ExternalRefComment: "This is the external ref for Acme",
+ },
+ },
+ PackageAttributionTexts: []string{
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.",
+ },
+ Files: nil,
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "PACKAGE-MANAGER",
+ RefType: "purl",
+ Locator: "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ },
+ },
+ FilesAnalyzed: false,
+ PackageHomePage: "http://www.openjena.org/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageVersion: "3.12.0",
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-Saxon",
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ PackageCopyrightText: "Copyright Saxonica Ltd",
+ PackageDescription: "The Saxon package is a collection of tools for processing XML documents.",
+ PackageDownloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://saxon.sourceforge.net/",
+ PackageLicenseComments: "Other versions available for a commercial license",
+ PackageLicenseConcluded: "MPL-1.0",
+ PackageLicenseDeclared: "MPL-1.0",
+ PackageName: "Saxon",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "8.8",
+ },
+ {
+ PrimaryPackagePurpose: "CONTAINER",
+ PackageSPDXIdentifier: "SPDXRef-CentOS-7",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDescription: "The CentOS container used to run the application.",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "https://www.centos.org/",
+ PackageName: "centos",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "centos7.9.2009",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ },
+ },
+ Files: []*v2_3.File{
+ {
+ FileName: "./src/org/spdx/parser/DOAPProject.java",
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ FileTypes: []string{
+ "SOURCE",
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright 2010, 2011 Source Auditor Inc.",
+ FileContributors: []string{
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c",
+ },
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-CommonsLangSrc",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file is used by Jena",
+ FileCopyrightText: "Copyright 2001-2011 The Apache Software Foundation",
+ FileContributors: []string{"Apache Software Foundation"},
+ FileName: "./lib-source/commons-lang3-3.1-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileNotice: "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-JenaLib",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file belongs to Jena",
+ FileCopyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ FileContributors: []string{"Apache Software Foundation", "Hewlett Packard Inc."},
+ FileName: "./lib-source/jena-2.6.3-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseComments: "This license is used by Jena",
+ LicenseConcluded: "LicenseRef-1",
+ LicenseInfoInFiles: []string{"LicenseRef-1"},
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-File",
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "File Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "File level annotation",
+ },
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ },
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ FileComment: "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ FileCopyrightText: "Copyright 2008-2010 John Smith",
+ FileContributors: []string{"The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"},
+ FileName: "./package/foo.c",
+ FileTypes: []string{"SOURCE"},
+ LicenseComments: "The concluded license was taken from the package level that the file was included in.",
+ LicenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)",
+ LicenseInfoInFiles: []string{"GPL-2.0-only", "LicenseRef-2"},
+ FileNotice: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ },
+ },
+ Snippets: []v2_3.Snippet{
+ {
+ SnippetSPDXIdentifier: "SPDXRef-Snippet",
+ SnippetFromFileSPDXIdentifier: "SPDXRef-DoapSource",
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{
+ Offset: 310,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ Offset: 420,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ {
+ StartPointer: common.SnippetRangePointer{
+ LineNumber: 5,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ LineNumber: 23,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-only",
+ LicenseInfoInSnippet: []string{"GPL-2.0-only"},
+ SnippetLicenseComments: "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ SnippetCopyrightText: "Copyright 2008-2010 John Smith",
+ SnippetComment: "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ SnippetName: "from linux kernel",
+ },
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("spdx-tool-1.2", "ToolsElement"),
+ Relationship: "COPY_OF",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "JenaLib"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "Saxon"),
+ Relationship: "DYNAMIC_LINK",
+ },
+ {
+ RefA: common.MakeDocElementID("", "CommonsLangSrc"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "GENERATED_FROM",
+ },
+ {
+ RefA: common.MakeDocElementID("", "JenaLib"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "File"),
+ RefB: common.MakeDocElementID("", "fromDoap-0"),
+ Relationship: "GENERATED_FROM",
+ },
+ },
+ Reviews: []*v2_3.Review{
+ {
+ Reviewer: "joe@example.com",
+ ReviewerType: "Person",
+ ReviewDate: "2021-11-03T05:43:21Z",
+ ReviewComment: "This is a review comment",
+ },
+ },
+}
diff --git a/tvloader/parser2v3/parse_annotation.go b/tvloader/parser2v3/parse_annotation.go
new file mode 100644
index 0000000..37861ba
--- /dev/null
+++ b/tvloader/parser2v3/parse_annotation.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+)
+
+func (parser *tvParser2_3) parsePairForAnnotation2_3(tag string, value string) error {
+ if parser.ann == nil {
+ return fmt.Errorf("no annotation struct created in parser ann pointer")
+ }
+
+ switch tag {
+ case "Annotator":
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ if subkey == "Person" || subkey == "Organization" || subkey == "Tool" {
+ parser.ann.Annotator.AnnotatorType = subkey
+ parser.ann.Annotator.Annotator = subvalue
+ return nil
+ }
+ return fmt.Errorf("unrecognized Annotator type %v", subkey)
+ case "AnnotationDate":
+ parser.ann.AnnotationDate = value
+ case "AnnotationType":
+ parser.ann.AnnotationType = value
+ case "SPDXREF":
+ deID, err := extractDocElementID(value)
+ if err != nil {
+ return err
+ }
+ parser.ann.AnnotationSPDXIdentifier = deID
+ case "AnnotationComment":
+ parser.ann.AnnotationComment = value
+ default:
+ return fmt.Errorf("received unknown tag %v in Annotation section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_annotation_test.go b/tvloader/parser2v3/parse_annotation_test.go
new file mode 100644
index 0000000..6681ed5
--- /dev/null
+++ b/tvloader/parser2v3/parse_annotation_test.go
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Annotation section tests =====
+func TestParser2_3FailsIfAnnotationNotSet(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePairForAnnotation2_3("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromAnnotation2_3 without setting ann pointer")
+ }
+}
+
+func TestParser2_3FailsIfAnnotationTagUnknown(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_3("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // parse invalid tag, using parsePairForAnnotation2_3(
+ err = parser.parsePairForAnnotation2_3("blah", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfAnnotationFieldsWithoutAnnotation(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("AnnotationDate", "2018-09-15T17:25:00Z")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for AnnotationDate without Annotator first")
+ }
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for AnnotationType without Annotator first")
+ }
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for SPDXREF without Annotator first")
+ }
+ err = parser.parsePair2_3("AnnotationComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_3 for AnnotationComment without Annotator first")
+ }
+}
+
+func TestParser2_3CanParseAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // Annotator without email address
+ err := parser.parsePair2_3("Annotator", "Person: John Doe")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.Annotator.Annotator != "John Doe" {
+ t.Errorf("got %+v for Annotator, expected John Doe", parser.ann.Annotator.Annotator)
+ }
+ if parser.ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf("got %v for AnnotatorType, expected Person", parser.ann.Annotator.AnnotatorType)
+ }
+
+ // Annotation Date
+ dt := "2018-09-15T17:32:00Z"
+ err = parser.parsePair2_3("AnnotationDate", dt)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.AnnotationDate != dt {
+ t.Errorf("got %v for AnnotationDate, expected %v", parser.ann.AnnotationDate, dt)
+ }
+
+ // Annotation type
+ aType := "REVIEW"
+ err = parser.parsePair2_3("AnnotationType", aType)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.AnnotationType != aType {
+ t.Errorf("got %v for AnnotationType, expected %v", parser.ann.AnnotationType, aType)
+ }
+
+ // SPDX Identifier Reference
+ ref := "SPDXRef-30"
+ err = parser.parsePair2_3("SPDXREF", ref)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ deID := parser.ann.AnnotationSPDXIdentifier
+ if deID.DocumentRefID != "" || deID.ElementRefID != "30" {
+ t.Errorf("got %v for SPDXREF, expected %v", parser.ann.AnnotationSPDXIdentifier, "30")
+ }
+
+ // Annotation Comment
+ cmt := "this is a comment"
+ err = parser.parsePair2_3("AnnotationComment", cmt)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.ann.AnnotationComment != cmt {
+ t.Errorf("got %v for AnnotationComment, expected %v", parser.ann.AnnotationComment, cmt)
+ }
+}
+
+func TestParser2_3FailsIfAnnotatorInvalid(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("Annotator", "John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfAnnotatorTypeInvalid(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("Annotator", "Human: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_3FailsIfAnnotationRefInvalid(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_3("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePair2_3("SPDXREF", "blah:other")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v3/parse_creation_info.go b/tvloader/parser2v3/parse_creation_info.go
new file mode 100644
index 0000000..693a56f
--- /dev/null
+++ b/tvloader/parser2v3/parse_creation_info.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *tvParser2_3) parsePairFromCreationInfo2_3(tag string, value string) error {
+ // fail if not in Creation Info parser state
+ if parser.st != psCreationInfo2_3 {
+ return fmt.Errorf("got invalid state %v in parsePairFromCreationInfo2_3", parser.st)
+ }
+
+ // create an SPDX Creation Info data struct if we don't have one already
+ if parser.doc.CreationInfo == nil {
+ parser.doc.CreationInfo = &v2_3.CreationInfo{}
+ }
+
+ ci := parser.doc.CreationInfo
+ switch tag {
+ case "LicenseListVersion":
+ ci.LicenseListVersion = value
+ case "Creator":
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+
+ creator := common.Creator{Creator: subvalue}
+ switch subkey {
+ case "Person", "Organization", "Tool":
+ creator.CreatorType = subkey
+ default:
+ return fmt.Errorf("unrecognized Creator type %v", subkey)
+ }
+
+ ci.Creators = append(ci.Creators, creator)
+ case "Created":
+ ci.Created = value
+ case "CreatorComment":
+ ci.CreatorComment = value
+
+ // tag for going on to package section
+ case "PackageName":
+ // error if last file does not have an identifier
+ // this may be a null case: can we ever have a "last file" in
+ // the "creation info" state? should go on to "file" state
+ // even when parsing unpackaged files.
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_3 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.st = psPackage2_3
+ parser.pkg = &v2_3.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ return parser.parsePairFromPackage2_3(tag, value)
+ // tag for going on to _unpackaged_ file section
+ case "FileName":
+ // leave pkg as nil, so that packages will be placed in Files
+ parser.st = psFile2_3
+ parser.pkg = nil
+ return parser.parsePairFromFile2_3(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_3
+ return parser.parsePairFromOtherLicense2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in CreationInfo section", tag)
+ }
+
+ return nil
+}
+
+// ===== Helper functions =====
+
+func extractExternalDocumentReference(value string) (string, string, string, string, error) {
+ sp := strings.Split(value, " ")
+ // remove any that are just whitespace
+ keepSp := []string{}
+ for _, s := range sp {
+ ss := strings.TrimSpace(s)
+ if ss != "" {
+ keepSp = append(keepSp, ss)
+ }
+ }
+
+ var documentRefID, uri, alg, checksum string
+
+ // now, should have 4 items (or 3, if Alg and Checksum were joined)
+ // and should be able to map them
+ if len(keepSp) == 4 {
+ documentRefID = keepSp[0]
+ uri = keepSp[1]
+ alg = keepSp[2]
+ // check that colon is present for alg, and remove it
+ if !strings.HasSuffix(alg, ":") {
+ return "", "", "", "", fmt.Errorf("algorithm does not end with colon")
+ }
+ alg = strings.TrimSuffix(alg, ":")
+ checksum = keepSp[3]
+ } else if len(keepSp) == 3 {
+ documentRefID = keepSp[0]
+ uri = keepSp[1]
+ // split on colon into alg and checksum
+ parts := strings.SplitN(keepSp[2], ":", 2)
+ if len(parts) != 2 {
+ return "", "", "", "", fmt.Errorf("missing colon separator between algorithm and checksum")
+ }
+ alg = parts[0]
+ checksum = parts[1]
+ } else {
+ return "", "", "", "", fmt.Errorf("expected 4 elements, got %d", len(keepSp))
+ }
+
+ // additionally, we should be able to parse the first element as a
+ // DocumentRef- ID string, and we should remove that prefix
+ if !strings.HasPrefix(documentRefID, "DocumentRef-") {
+ return "", "", "", "", fmt.Errorf("expected first element to have DocumentRef- prefix")
+ }
+ documentRefID = strings.TrimPrefix(documentRefID, "DocumentRef-")
+ if documentRefID == "" {
+ return "", "", "", "", fmt.Errorf("document identifier has nothing after prefix")
+ }
+
+ return documentRefID, uri, alg, checksum, nil
+}
diff --git a/tvloader/parser2v3/parse_creation_info_test.go b/tvloader/parser2v3/parse_creation_info_test.go
new file mode 100644
index 0000000..24cb80a
--- /dev/null
+++ b/tvloader/parser2v3/parse_creation_info_test.go
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Parser creation info state change tests =====
+func TestParser2_3CIMovesToPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ pkgName := "testPkg"
+ err := parser.parsePair2_3("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_3)
+ }
+ // and a package should be created
+ if parser.pkg == nil {
+ t.Fatalf("parser didn't create new package")
+ }
+ // and the package name should be as expected
+ if parser.pkg.PackageName != pkgName {
+ t.Errorf("expected package name %s, got %s", pkgName, parser.pkg.PackageName)
+ }
+ // and the package should default to true for FilesAnalyzed
+ if parser.pkg.FilesAnalyzed != true {
+ t.Errorf("expected FilesAnalyzed to default to true, got false")
+ }
+ if parser.pkg.IsFilesAnalyzedTagPresent != false {
+ t.Errorf("expected IsFilesAnalyzedTagPresent to default to false, got true")
+ }
+ // and the package should NOT be in the SPDX Document's map of packages,
+ // because it doesn't have an SPDX identifier yet
+ if len(parser.doc.Packages) != 0 {
+ t.Errorf("expected 0 packages, got %d", len(parser.doc.Packages))
+ }
+}
+
+func TestParser2_3CIMovesToFileAfterParsingFileNameTagWithNoPackages(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_3)
+ }
+ // and current package should be nil, meaning Files are placed in the
+ // Files map instead of in a Package
+ if parser.pkg != nil {
+ t.Fatalf("expected pkg to be nil, got non-nil pkg")
+ }
+}
+
+func TestParser2_3CIMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psOtherLicense2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_3)
+ }
+}
+
+func TestParser2_3CIMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePair2_3("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psReview2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_3)
+ }
+}
+
+func TestParser2_3CIStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+}
+
+func TestParser2_3CIStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+
+ err = parser.parsePair2_3("AnnotationComment", "i guess i had something to say about this spdx file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.st != psCreationInfo2_3 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_3)
+ }
+}
+
+func TestParser2_3FailsParsingCreationInfoWithInvalidState(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psPackage2_3,
+ }
+ err := parser.parsePairFromCreationInfo2_3("SPDXVersion", "SPDX-2.3")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+// ===== Creation Info section tests =====
+func TestParser2_3HasCreationInfoAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+ err := parser.parsePairFromCreationInfo2_3("LicenseListVersion", "3.9")
+ if err != nil {
+ t.Errorf("got error when calling parsePairFromCreationInfo2_3: %v", err)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_3CanParseCreationInfoTags(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ // License List Version
+ err := parser.parsePairFromCreationInfo2_3("LicenseListVersion", "2.3")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.LicenseListVersion != "2.3" {
+ t.Errorf("got %v for LicenseListVersion", parser.doc.CreationInfo.LicenseListVersion)
+ }
+
+ // Creators: Persons
+ refPersons := []string{
+ "Person: Person A",
+ "Person: Person B",
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refPersons[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refPersons[1])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.doc.CreationInfo.Creators) != 2 ||
+ parser.doc.CreationInfo.Creators[0].Creator != "Person A" ||
+ parser.doc.CreationInfo.Creators[1].Creator != "Person B" {
+ t.Errorf("got %v for CreatorPersons", parser.doc.CreationInfo.Creators)
+ }
+
+ // Creators: Organizations
+ refOrgs := []string{
+ "Organization: Organization A",
+ "Organization: Organization B",
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refOrgs[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refOrgs[1])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.doc.CreationInfo.Creators) != 4 ||
+ parser.doc.CreationInfo.Creators[2].Creator != "Organization A" ||
+ parser.doc.CreationInfo.Creators[3].Creator != "Organization B" {
+ t.Errorf("got %v for CreatorOrganizations", parser.doc.CreationInfo.Creators)
+ }
+
+ // Creators: Tools
+ refTools := []string{
+ "Tool: Tool A",
+ "Tool: Tool B",
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refTools[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_3("Creator", refTools[1])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if len(parser.doc.CreationInfo.Creators) != 6 ||
+ parser.doc.CreationInfo.Creators[4].Creator != "Tool A" ||
+ parser.doc.CreationInfo.Creators[5].Creator != "Tool B" {
+ t.Errorf("got %v for CreatorTools", parser.doc.CreationInfo.Creators)
+ }
+
+ // Created date
+ err = parser.parsePairFromCreationInfo2_3("Created", "2018-09-10T11:46:00Z")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.Created != "2018-09-10T11:46:00Z" {
+ t.Errorf("got %v for Created", parser.doc.CreationInfo.Created)
+ }
+
+ // Creator Comment
+ err = parser.parsePairFromCreationInfo2_3("CreatorComment", "Blah whatever")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.CreatorComment != "Blah whatever" {
+ t.Errorf("got %v for CreatorComment", parser.doc.CreationInfo.CreatorComment)
+ }
+}
+
+func TestParser2_3InvalidCreatorTagsFail(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePairFromCreationInfo2_3("Creator", "blah: somebody")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+
+ err = parser.parsePairFromCreationInfo2_3("Creator", "Tool with no colons")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+}
+
+func TestParser2_3CreatorTagWithMultipleColonsPasses(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePairFromCreationInfo2_3("Creator", "Tool: tool1:2:3")
+ if err != nil {
+ t.Errorf("unexpected error from parsing valid Creator format")
+ }
+}
+
+func TestParser2_3CIUnknownTagFails(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePairFromCreationInfo2_3("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_3CICreatesRelationship(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.rln == nil {
+ t.Fatalf("parser didn't create and point to Relationship struct")
+ }
+ if parser.rln != parser.doc.Relationships[0] {
+ t.Errorf("pointer to new Relationship doesn't match idx 0 for doc.Relationships[]")
+ }
+}
+
+func TestParser2_3CICreatesAnnotation(t *testing.T) {
+ parser := tvParser2_3{
+ doc: &v2_3.Document{},
+ st: psCreationInfo2_3,
+ }
+
+ err := parser.parsePair2_3("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_3: %v", err)
+ }
+ if parser.ann == nil {
+ t.Fatalf("parser didn't create and point to Annotation struct")
+ }
+ if parser.ann != parser.doc.Annotations[0] {
+ t.Errorf("pointer to new Annotation doesn't match idx 0 for doc.Annotations[]")
+ }
+}
+
+// ===== Helper function tests =====
+
+func TestCanExtractExternalDocumentReference(t *testing.T) {
+ refstring := "DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759"
+ wantDocumentRefID := "spdx-tool-1.2"
+ wantURI := "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+ wantAlg := "SHA1"
+ wantChecksum := "d6a770ba38583ed4bb4525bd96e50461655d2759"
+
+ gotDocumentRefID, gotURI, gotAlg, gotChecksum, err := extractExternalDocumentReference(refstring)
+ if err != nil {
+ t.Errorf("got non-nil error: %v", err)
+ }
+ if wantDocumentRefID != gotDocumentRefID {
+ t.Errorf("wanted document ref ID %s, got %s", wantDocumentRefID, gotDocumentRefID)
+ }
+ if wantURI != gotURI {
+ t.Errorf("wanted URI %s, got %s", wantURI, gotURI)
+ }
+ if wantAlg != gotAlg {
+ t.Errorf("wanted alg %s, got %s", wantAlg, gotAlg)
+ }
+ if wantChecksum != gotChecksum {
+ t.Errorf("wanted checksum %s, got %s", wantChecksum, gotChecksum)
+ }
+}
+
+func TestCanExtractExternalDocumentReferenceWithExtraWhitespace(t *testing.T) {
+ refstring := " DocumentRef-spdx-tool-1.2 \t http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 \t SHA1: \t d6a770ba38583ed4bb4525bd96e50461655d2759"
+ wantDocumentRefID := "spdx-tool-1.2"
+ wantURI := "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+ wantAlg := "SHA1"
+ wantChecksum := "d6a770ba38583ed4bb4525bd96e50461655d2759"
+
+ gotDocumentRefID, gotURI, gotAlg, gotChecksum, err := extractExternalDocumentReference(refstring)
+ if err != nil {
+ t.Errorf("got non-nil error: %v", err)
+ }
+ if wantDocumentRefID != gotDocumentRefID {
+ t.Errorf("wanted document ref ID %s, got %s", wantDocumentRefID, gotDocumentRefID)
+ }
+ if wantURI != gotURI {
+ t.Errorf("wanted URI %s, got %s", wantURI, gotURI)
+ }
+ if wantAlg != gotAlg {
+ t.Errorf("wanted alg %s, got %s", wantAlg, gotAlg)
+ }
+ if wantChecksum != gotChecksum {
+ t.Errorf("wanted checksum %s, got %s", wantChecksum, gotChecksum)
+ }
+}
+
+func TestFailsExternalDocumentReferenceWithInvalidFormats(t *testing.T) {
+ invalidRefs := []string{
+ "whoops",
+ "DocumentRef-",
+ "DocumentRef- ",
+ "DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ "DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 d6a770ba38583ed4bb4525bd96e50461655d2759",
+ "DocumentRef-spdx-tool-1.2",
+ "spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759",
+ }
+ for _, refstring := range invalidRefs {
+ _, _, _, _, err := extractExternalDocumentReference(refstring)
+ if err == nil {
+ t.Errorf("expected non-nil error for %s, got nil", refstring)
+ }
+ }
+}
diff --git a/tvloader/parser2v3/parse_file.go b/tvloader/parser2v3/parse_file.go
new file mode 100644
index 0000000..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
+}
diff --git a/tvsaver/saver2v3/save_annotation.go b/tvsaver/saver2v3/save_annotation.go
new file mode 100644
index 0000000..6c4f1aa
--- /dev/null
+++ b/tvsaver/saver2v3/save_annotation.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderAnnotation2_3(ann *v2_3.Annotation, w io.Writer) error {
+ if ann.Annotator.Annotator != "" && ann.Annotator.AnnotatorType != "" {
+ fmt.Fprintf(w, "Annotator: %s: %s\n", ann.Annotator.AnnotatorType, ann.Annotator.Annotator)
+ }
+ if ann.AnnotationDate != "" {
+ fmt.Fprintf(w, "AnnotationDate: %s\n", ann.AnnotationDate)
+ }
+ if ann.AnnotationType != "" {
+ fmt.Fprintf(w, "AnnotationType: %s\n", ann.AnnotationType)
+ }
+ annIDStr := common.RenderDocElementID(ann.AnnotationSPDXIdentifier)
+ if annIDStr != "SPDXRef-" {
+ fmt.Fprintf(w, "SPDXREF: %s\n", annIDStr)
+ }
+ if ann.AnnotationComment != "" {
+ fmt.Fprintf(w, "AnnotationComment: %s\n", textify(ann.AnnotationComment))
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_annotation_test.go b/tvsaver/saver2v3/save_annotation_test.go
new file mode 100644
index 0000000..7471260
--- /dev/null
+++ b/tvsaver/saver2v3/save_annotation_test.go
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Annotation section Saver tests =====
+func TestSaver2_3AnnotationSavesTextForPerson(t *testing.T) {
+ ann := &v2_3.Annotation{
+ Annotator: common.Annotator{AnnotatorType: "Person", Annotator: "John Doe"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Annotator: Person: John Doe
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderAnnotation2_3(ann, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3AnnotationSavesTextForOrganization(t *testing.T) {
+ ann := &v2_3.Annotation{
+ Annotator: common.Annotator{AnnotatorType: "Organization", Annotator: "John Doe, Inc."},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Annotator: Organization: John Doe, Inc.
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderAnnotation2_3(ann, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3AnnotationSavesTextForTool(t *testing.T) {
+ ann := &v2_3.Annotation{
+ Annotator: common.Annotator{AnnotatorType: "Tool", Annotator: "magictool-1.1"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Annotator: Tool: magictool-1.1
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderAnnotation2_3(ann, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+// note that the annotation has no optional or multiple fields
diff --git a/tvsaver/saver2v3/save_creation_info.go b/tvsaver/saver2v3/save_creation_info.go
new file mode 100644
index 0000000..2e8037d
--- /dev/null
+++ b/tvsaver/saver2v3/save_creation_info.go
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderCreationInfo2_3(ci *v2_3.CreationInfo, w io.Writer) error {
+ if ci.LicenseListVersion != "" {
+ fmt.Fprintf(w, "LicenseListVersion: %s\n", ci.LicenseListVersion)
+ }
+ for _, creator := range ci.Creators {
+ fmt.Fprintf(w, "Creator: %s: %s\n", creator.CreatorType, creator.Creator)
+ }
+ if ci.Created != "" {
+ fmt.Fprintf(w, "Created: %s\n", ci.Created)
+ }
+ if ci.CreatorComment != "" {
+ fmt.Fprintf(w, "CreatorComment: %s\n", textify(ci.CreatorComment))
+ }
+
+ // add blank newline b/c end of a main section
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_creation_info_test.go b/tvsaver/saver2v3/save_creation_info_test.go
new file mode 100644
index 0000000..a433dc5
--- /dev/null
+++ b/tvsaver/saver2v3/save_creation_info_test.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Creation Info section Saver tests =====
+func TestSaver2_3CISavesText(t *testing.T) {
+ ci := &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ {Creator: "Jane Doe (janedoe@example.com)", CreatorType: "Person"},
+ {Creator: "John Doe, Inc.", CreatorType: "Organization"},
+ {Creator: "Jane Doe LLC", CreatorType: "Organization"},
+ {Creator: "magictool1-1.0", CreatorType: "Tool"},
+ {Creator: "magictool2-1.0", CreatorType: "Tool"},
+ {Creator: "magictool3-1.0", CreatorType: "Tool"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ CreatorComment: "this is a creator comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`LicenseListVersion: 3.9
+Creator: Person: John Doe
+Creator: Person: Jane Doe (janedoe@example.com)
+Creator: Organization: John Doe, Inc.
+Creator: Organization: Jane Doe LLC
+Creator: Tool: magictool1-1.0
+Creator: Tool: magictool2-1.0
+Creator: Tool: magictool3-1.0
+Created: 2018-10-10T06:20:00Z
+CreatorComment: this is a creator comment
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderCreationInfo2_3(ci, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3CIOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ // --- need at least one creator; do first for Persons ---
+ ci1 := &v2_3.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want1 := bytes.NewBufferString(`Creator: Person: John Doe
+Created: 2018-10-10T06:20:00Z
+
+`)
+
+ // render as buffer of bytes
+ var got1 bytes.Buffer
+ err := renderCreationInfo2_3(ci1, &got1)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c1 := bytes.Compare(want1.Bytes(), got1.Bytes())
+ if c1 != 0 {
+ t.Errorf("Expected %v, got %v", want1.String(), got1.String())
+ }
+
+ // --- need at least one creator; now switch to organization ---
+ ci2 := &v2_3.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe, Inc.", CreatorType: "Organization"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want2 := bytes.NewBufferString(`Creator: Organization: John Doe, Inc.
+Created: 2018-10-10T06:20:00Z
+
+`)
+
+ // render as buffer of bytes
+ var got2 bytes.Buffer
+ err = renderCreationInfo2_3(ci2, &got2)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c2 := bytes.Compare(want2.Bytes(), got2.Bytes())
+ if c2 != 0 {
+ t.Errorf("Expected %v, got %v", want2.String(), got2.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_document.go b/tvsaver/saver2v3/save_document.go
new file mode 100644
index 0000000..e8c2535
--- /dev/null
+++ b/tvsaver/saver2v3/save_document.go
@@ -0,0 +1,104 @@
+// Package saver2v3 contains functions to render and write a tag-value
+// formatted version of an in-memory SPDX document and its sections
+// (version 2.2).
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// RenderDocument2_3 is the main entry point to take an SPDX in-memory
+// Document (version 2.2), and render it to the received io.Writer.
+// It is only exported in order to be available to the tvsaver package,
+// and typically does not need to be called by client code.
+func RenderDocument2_3(doc *v2_3.Document, w io.Writer) error {
+ if doc.CreationInfo == nil {
+ return fmt.Errorf("Document had nil CreationInfo section")
+ }
+
+ if doc.SPDXVersion != "" {
+ fmt.Fprintf(w, "SPDXVersion: %s\n", doc.SPDXVersion)
+ }
+ if doc.DataLicense != "" {
+ fmt.Fprintf(w, "DataLicense: %s\n", doc.DataLicense)
+ }
+ if doc.SPDXIdentifier != "" {
+ fmt.Fprintf(w, "SPDXID: %s\n", common.RenderElementID(doc.SPDXIdentifier))
+ }
+ if doc.DocumentName != "" {
+ fmt.Fprintf(w, "DocumentName: %s\n", doc.DocumentName)
+ }
+ if doc.DocumentNamespace != "" {
+ fmt.Fprintf(w, "DocumentNamespace: %s\n", doc.DocumentNamespace)
+ }
+ // print EDRs in order sorted by identifier
+ sort.Slice(doc.ExternalDocumentReferences, func(i, j int) bool {
+ return doc.ExternalDocumentReferences[i].DocumentRefID < doc.ExternalDocumentReferences[j].DocumentRefID
+ })
+ for _, edr := range doc.ExternalDocumentReferences {
+ fmt.Fprintf(w, "ExternalDocumentRef: DocumentRef-%s %s %s:%s\n",
+ edr.DocumentRefID, edr.URI, edr.Checksum.Algorithm, edr.Checksum.Value)
+ }
+ if doc.DocumentComment != "" {
+ fmt.Fprintf(w, "DocumentComment: %s\n", textify(doc.DocumentComment))
+ }
+
+ renderCreationInfo2_3(doc.CreationInfo, w)
+
+ if len(doc.Files) > 0 {
+ fmt.Fprintf(w, "##### Unpackaged files\n\n")
+ sort.Slice(doc.Files, func(i, j int) bool {
+ return doc.Files[i].FileSPDXIdentifier < doc.Files[j].FileSPDXIdentifier
+ })
+ for _, fi := range doc.Files {
+ renderFile2_3(fi, w)
+ }
+ }
+
+ // sort Packages by identifier
+ sort.Slice(doc.Packages, func(i, j int) bool {
+ return doc.Packages[i].PackageSPDXIdentifier < doc.Packages[j].PackageSPDXIdentifier
+ })
+ for _, pkg := range doc.Packages {
+ fmt.Fprintf(w, "##### Package: %s\n\n", pkg.PackageName)
+ renderPackage2_3(pkg, w)
+ }
+
+ if len(doc.OtherLicenses) > 0 {
+ fmt.Fprintf(w, "##### Other Licenses\n\n")
+ for _, ol := range doc.OtherLicenses {
+ renderOtherLicense2_3(ol, w)
+ }
+ }
+
+ if len(doc.Relationships) > 0 {
+ fmt.Fprintf(w, "##### Relationships\n\n")
+ for _, rln := range doc.Relationships {
+ renderRelationship2_3(rln, w)
+ }
+ fmt.Fprintf(w, "\n")
+ }
+
+ if len(doc.Annotations) > 0 {
+ fmt.Fprintf(w, "##### Annotations\n\n")
+ for _, ann := range doc.Annotations {
+ renderAnnotation2_3(ann, w)
+ fmt.Fprintf(w, "\n")
+ }
+ }
+
+ if len(doc.Reviews) > 0 {
+ fmt.Fprintf(w, "##### Reviews\n\n")
+ for _, rev := range doc.Reviews {
+ renderReview2_3(rev, w)
+ }
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_document_test.go b/tvsaver/saver2v3/save_document_test.go
new file mode 100644
index 0000000..10aa311
--- /dev/null
+++ b/tvsaver/saver2v3/save_document_test.go
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== entire Document Saver tests =====
+func TestSaver2_3DocumentSavesText(t *testing.T) {
+
+ // Creation Info section
+ ci := &v2_3.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // unpackaged files
+ f1 := &v2_3.File{
+ FileName: "/tmp/whatever1.txt",
+ FileSPDXIdentifier: common.ElementID("File1231"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983c", Algorithm: common.SHA1}},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ }
+
+ f2 := &v2_3.File{
+ FileName: "/tmp/whatever2.txt",
+ FileSPDXIdentifier: common.ElementID("File1232"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983d", Algorithm: common.SHA1}},
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{"MIT"},
+ FileCopyrightText: "Copyright (c) John Doe",
+ }
+
+ unFiles := []*v2_3.File{
+ f1,
+ f2,
+ }
+
+ // Package 1: packaged files with snippets
+ sn1 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: "Snippet19",
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "FileHasSnippets").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 17}, EndPointer: common.SnippetRangePointer{Offset: 209}}},
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ }
+
+ sn2 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: "Snippet20",
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "FileHasSnippets").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 268}, EndPointer: common.SnippetRangePointer{Offset: 309}}},
+ SnippetLicenseConcluded: "WTFPL",
+ SnippetCopyrightText: "NOASSERTION",
+ }
+
+ f3 := &v2_3.File{
+ FileName: "/tmp/file-with-snippets.txt",
+ FileSPDXIdentifier: common.ElementID("FileHasSnippets"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983e", Algorithm: common.SHA1}},
+ LicenseConcluded: "GPL-2.0-or-later AND WTFPL",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "WTFPL",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ Snippets: map[common.ElementID]*v2_3.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ },
+ }
+
+ f4 := &v2_3.File{
+ FileName: "/tmp/another-file.txt",
+ FileSPDXIdentifier: common.ElementID("FileAnother"),
+ Checksums: []common.Checksum{{Value: "85ed0817af83a24ad8da68c2b5094de69833983f", Algorithm: common.SHA1}},
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{"BSD-3-Clause"},
+ FileCopyrightText: "Copyright (c) Jane Doe LLC",
+ }
+
+ pkgWith := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "0123456789abcdef0123456789abcdef01234567"},
+ PackageLicenseConcluded: "GPL-2.0-or-later AND BSD-3-Clause AND WTFPL",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ "WTFPL",
+ "BSD-3-Clause",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ Files: []*v2_3.File{
+ f3,
+ f4,
+ },
+ }
+
+ // Other Licenses 1 and 2
+ ol1 := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ }
+
+ ol2 := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: `License 2 text - this is a license that does some stuff`,
+ LicenseName: "License 2",
+ }
+
+ // Relationships
+ rln1 := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln2 := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1231"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln3 := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1232"),
+ Relationship: "DESCRIBES",
+ }
+
+ // Annotations
+ ann1 := &v2_3.Annotation{
+ Annotator: common.Annotator{Annotator: "John Doe",
+ AnnotatorType: "Person"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "DOCUMENT"),
+ AnnotationComment: "This is an annotation about the SPDX document",
+ }
+
+ ann2 := &v2_3.Annotation{
+ Annotator: common.Annotator{Annotator: "John Doe, Inc.",
+ AnnotatorType: "Organization"},
+ AnnotationDate: "2018-10-10T17:52:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationSPDXIdentifier: common.MakeDocElementID("", "p1"),
+ AnnotationComment: "This is an annotation about Package p1",
+ }
+
+ // Reviews
+ rev1 := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ }
+ rev2 := &v2_3.Review{
+ Reviewer: "Jane Doe LLC",
+ ReviewerType: "Organization",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ ReviewComment: "I have reviewed this SPDX document and it is awesome",
+ }
+
+ // now, build the document
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: "tools-golang-0.0.1.abcdef",
+ DocumentNamespace: "https://github.com/spdx/spdx-docs/tools-golang/tools-golang-0.0.1.abcdef.whatever",
+ CreationInfo: ci,
+ Packages: []*v2_3.Package{
+ pkgWith,
+ },
+ Files: unFiles,
+ OtherLicenses: []*v2_3.OtherLicense{
+ ol1,
+ ol2,
+ },
+ Relationships: []*v2_3.Relationship{
+ rln1,
+ rln2,
+ rln3,
+ },
+ Annotations: []*v2_3.Annotation{
+ ann1,
+ ann2,
+ },
+ Reviews: []*v2_3.Review{
+ rev1,
+ rev2,
+ },
+ }
+
+ want := bytes.NewBufferString(`SPDXVersion: SPDX-2.2
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: tools-golang-0.0.1.abcdef
+DocumentNamespace: https://github.com/spdx/spdx-docs/tools-golang/tools-golang-0.0.1.abcdef.whatever
+Creator: Person: John Doe
+Created: 2018-10-10T06:20:00Z
+
+##### Unpackaged files
+
+FileName: /tmp/whatever1.txt
+SPDXID: SPDXRef-File1231
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+FileName: /tmp/whatever2.txt
+SPDXID: SPDXRef-File1232
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983d
+LicenseConcluded: MIT
+LicenseInfoInFile: MIT
+FileCopyrightText: Copyright (c) John Doe
+
+##### Package: p1
+
+PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: true
+PackageVerificationCode: 0123456789abcdef0123456789abcdef01234567
+PackageLicenseConcluded: GPL-2.0-or-later AND BSD-3-Clause AND WTFPL
+PackageLicenseInfoFromFiles: Apache-2.0
+PackageLicenseInfoFromFiles: GPL-2.0-or-later
+PackageLicenseInfoFromFiles: WTFPL
+PackageLicenseInfoFromFiles: BSD-3-Clause
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+
+FileName: /tmp/another-file.txt
+SPDXID: SPDXRef-FileAnother
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983f
+LicenseConcluded: BSD-3-Clause
+LicenseInfoInFile: BSD-3-Clause
+FileCopyrightText: Copyright (c) Jane Doe LLC
+
+FileName: /tmp/file-with-snippets.txt
+SPDXID: SPDXRef-FileHasSnippets
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983e
+LicenseConcluded: GPL-2.0-or-later AND WTFPL
+LicenseInfoInFile: Apache-2.0
+LicenseInfoInFile: GPL-2.0-or-later
+LicenseInfoInFile: WTFPL
+FileCopyrightText: Copyright (c) Jane Doe
+
+SnippetSPDXID: SPDXRef-Snippet19
+SnippetFromFileSPDXID: SPDXRef-FileHasSnippets
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+
+SnippetSPDXID: SPDXRef-Snippet20
+SnippetFromFileSPDXID: SPDXRef-FileHasSnippets
+SnippetByteRange: 268:309
+SnippetLicenseConcluded: WTFPL
+SnippetCopyrightText: NOASSERTION
+
+##### Other Licenses
+
+LicenseID: LicenseRef-1
+ExtractedText: <text>License 1 text
+blah blah blah
+blah blah blah blah</text>
+LicenseName: License 1
+
+LicenseID: LicenseRef-2
+ExtractedText: License 2 text - this is a license that does some stuff
+LicenseName: License 2
+
+##### Relationships
+
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-p1
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File1231
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File1232
+
+##### Annotations
+
+Annotator: Person: John Doe
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+AnnotationComment: This is an annotation about the SPDX document
+
+Annotator: Organization: John Doe, Inc.
+AnnotationDate: 2018-10-10T17:52:00Z
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-p1
+AnnotationComment: This is an annotation about Package p1
+
+##### Reviews
+
+Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+
+Reviewer: Organization: Jane Doe LLC
+ReviewDate: 2018-10-14T10:28:00Z
+ReviewComment: I have reviewed this SPDX document and it is awesome
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := RenderDocument2_3(doc, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected {{{%v}}}, got {{{%v}}}", want.String(), got.String())
+ }
+
+}
+
+func TestSaver2_3DocumentReturnsErrorIfNilCreationInfo(t *testing.T) {
+ doc := &v2_3.Document{}
+
+ var got bytes.Buffer
+ err := RenderDocument2_3(doc, &got)
+ if err == nil {
+ t.Errorf("Expected error, got nil")
+ }
+}
diff --git a/tvsaver/saver2v3/save_file.go b/tvsaver/saver2v3/save_file.go
new file mode 100644
index 0000000..18a6d98
--- /dev/null
+++ b/tvsaver/saver2v3/save_file.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderFile2_3(f *v2_3.File, w io.Writer) error {
+ if f.FileName != "" {
+ fmt.Fprintf(w, "FileName: %s\n", f.FileName)
+ }
+ if f.FileSPDXIdentifier != "" {
+ fmt.Fprintf(w, "SPDXID: %s\n", common.RenderElementID(f.FileSPDXIdentifier))
+ }
+ for _, s := range f.FileTypes {
+ fmt.Fprintf(w, "FileType: %s\n", s)
+ }
+
+ for _, checksum := range f.Checksums {
+ fmt.Fprintf(w, "FileChecksum: %s: %s\n", checksum.Algorithm, checksum.Value)
+ }
+
+ if f.LicenseConcluded != "" {
+ fmt.Fprintf(w, "LicenseConcluded: %s\n", f.LicenseConcluded)
+ }
+ for _, s := range f.LicenseInfoInFiles {
+ fmt.Fprintf(w, "LicenseInfoInFile: %s\n", s)
+ }
+ if f.LicenseComments != "" {
+ fmt.Fprintf(w, "LicenseComments: %s\n", textify(f.LicenseComments))
+ }
+ if f.FileCopyrightText != "" {
+ fmt.Fprintf(w, "FileCopyrightText: %s\n", textify(f.FileCopyrightText))
+ }
+ for _, aop := range f.ArtifactOfProjects {
+ fmt.Fprintf(w, "ArtifactOfProjectName: %s\n", aop.Name)
+ if aop.HomePage != "" {
+ fmt.Fprintf(w, "ArtifactOfProjectHomePage: %s\n", aop.HomePage)
+ }
+ if aop.URI != "" {
+ fmt.Fprintf(w, "ArtifactOfProjectURI: %s\n", aop.URI)
+ }
+ }
+ if f.FileComment != "" {
+ fmt.Fprintf(w, "FileComment: %s\n", textify(f.FileComment))
+ }
+ if f.FileNotice != "" {
+ fmt.Fprintf(w, "FileNotice: %s\n", textify(f.FileNotice))
+ }
+ for _, s := range f.FileContributors {
+ fmt.Fprintf(w, "FileContributor: %s\n", s)
+ }
+ for _, s := range f.FileAttributionTexts {
+ fmt.Fprintf(w, "FileAttributionText: %s\n", textify(s))
+ }
+ for _, s := range f.FileDependencies {
+ fmt.Fprintf(w, "FileDependency: %s\n", s)
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ // also render any snippets for this file
+ // get slice of Snippet identifiers so we can sort them
+ snippetKeys := []string{}
+ for k := range f.Snippets {
+ snippetKeys = append(snippetKeys, string(k))
+ }
+ sort.Strings(snippetKeys)
+ for _, sID := range snippetKeys {
+ s := f.Snippets[common.ElementID(sID)]
+ renderSnippet2_3(s, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_file_test.go b/tvsaver/saver2v3/save_file_test.go
new file mode 100644
index 0000000..05fc2cf
--- /dev/null
+++ b/tvsaver/saver2v3/save_file_test.go
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== File section Saver tests =====
+func TestSaver2_3FileSavesText(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ FileTypes: []string{
+ "TEXT",
+ "DOCUMENTATION",
+ },
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ {Algorithm: common.SHA256, Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"},
+ {Algorithm: common.MD5, Value: "624c1abb3664f4b35547e7c73864ad24"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ "Apache-1.1",
+ },
+ LicenseComments: "this is a license comment(s)",
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ ArtifactOfProjects: []*v2_3.ArtifactOfProject{
+ {
+ Name: "project1",
+ HomePage: "http://example.com/1/",
+ URI: "http://example.com/1/uri.whatever",
+ },
+ {
+ Name: "project2",
+ },
+ {
+ Name: "project3",
+ HomePage: "http://example.com/3/",
+ },
+ {
+ Name: "project4",
+ URI: "http://example.com/4/uri.whatever",
+ },
+ },
+ FileComment: "this is a file comment",
+ FileNotice: "This file may be used under either Apache-2.0 or Apache-1.1.",
+ FileContributors: []string{
+ "John Doe jdoe@example.com",
+ "EvilCorp",
+ },
+ FileAttributionTexts: []string{
+ "attributions",
+ `multi-line
+attribution`,
+ },
+ FileDependencies: []string{
+ "f-1.txt",
+ "g.txt",
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileType: TEXT
+FileType: DOCUMENTATION
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+FileChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+FileChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+LicenseInfoInFile: Apache-1.1
+LicenseComments: this is a license comment(s)
+FileCopyrightText: Copyright (c) Jane Doe
+ArtifactOfProjectName: project1
+ArtifactOfProjectHomePage: http://example.com/1/
+ArtifactOfProjectURI: http://example.com/1/uri.whatever
+ArtifactOfProjectName: project2
+ArtifactOfProjectName: project3
+ArtifactOfProjectHomePage: http://example.com/3/
+ArtifactOfProjectName: project4
+ArtifactOfProjectURI: http://example.com/4/uri.whatever
+FileComment: this is a file comment
+FileNotice: This file may be used under either Apache-2.0 or Apache-1.1.
+FileContributor: John Doe jdoe@example.com
+FileContributor: EvilCorp
+FileAttributionText: attributions
+FileAttributionText: <text>multi-line
+attribution</text>
+FileDependency: f-1.txt
+FileDependency: g.txt
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileSavesSnippetsAlso(t *testing.T) {
+ sn1 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet19"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File123").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 17}, EndPointer: common.SnippetRangePointer{Offset: 209}}},
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ }
+
+ sn2 := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet20"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File123").ElementRefID,
+ Ranges: []common.SnippetRange{{StartPointer: common.SnippetRangePointer{Offset: 268}, EndPointer: common.SnippetRangePointer{Offset: 309}}},
+ SnippetLicenseConcluded: "WTFPL",
+ SnippetCopyrightText: "NOASSERTION",
+ }
+
+ sns := map[common.ElementID]*v2_3.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ }
+
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ Snippets: sns,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+SnippetSPDXID: SPDXRef-Snippet19
+SnippetFromFileSPDXID: SPDXRef-File123
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+
+SnippetSPDXID: SPDXRef-Snippet20
+SnippetFromFileSPDXID: SPDXRef-File123
+SnippetByteRange: 268:309
+SnippetLicenseConcluded: WTFPL
+SnippetCopyrightText: NOASSERTION
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileWrapsCopyrightMultiLine(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: `Copyright (c) Jane Doe
+Copyright (c) John Doe`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: <text>Copyright (c) Jane Doe
+Copyright (c) John Doe</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3FileWrapsCommentsAndNoticesMultiLine(t *testing.T) {
+ f := &v2_3.File{
+ FileName: "/tmp/whatever.txt",
+ FileSPDXIdentifier: common.ElementID("File123"),
+ Checksums: []common.Checksum{
+ {Algorithm: common.SHA1, Value: "85ed0817af83a24ad8da68c2b5094de69833983c"},
+ },
+ LicenseComments: `this is a
+multi-line license comment`,
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ FileComment: `this is a
+multi-line file comment`,
+ FileNotice: `This file may be used
+under either Apache-2.0 or Apache-1.1.`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`FileName: /tmp/whatever.txt
+SPDXID: SPDXRef-File123
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+LicenseComments: <text>this is a
+multi-line license comment</text>
+FileCopyrightText: Copyright (c) Jane Doe
+FileComment: <text>this is a
+multi-line file comment</text>
+FileNotice: <text>This file may be used
+under either Apache-2.0 or Apache-1.1.</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_3(f, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_other_license.go b/tvsaver/saver2v3/save_other_license.go
new file mode 100644
index 0000000..9351108
--- /dev/null
+++ b/tvsaver/saver2v3/save_other_license.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderOtherLicense2_3(ol *v2_3.OtherLicense, w io.Writer) error {
+ if ol.LicenseIdentifier != "" {
+ fmt.Fprintf(w, "LicenseID: %s\n", ol.LicenseIdentifier)
+ }
+ if ol.ExtractedText != "" {
+ fmt.Fprintf(w, "ExtractedText: %s\n", textify(ol.ExtractedText))
+ }
+ if ol.LicenseName != "" {
+ fmt.Fprintf(w, "LicenseName: %s\n", ol.LicenseName)
+ }
+ for _, s := range ol.LicenseCrossReferences {
+ fmt.Fprintf(w, "LicenseCrossReference: %s\n", s)
+ }
+ if ol.LicenseComment != "" {
+ fmt.Fprintf(w, "LicenseComment: %s\n", textify(ol.LicenseComment))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_other_license_test.go b/tvsaver/saver2v3/save_other_license_test.go
new file mode 100644
index 0000000..00134dd
--- /dev/null
+++ b/tvsaver/saver2v3/save_other_license_test.go
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Other License section Saver tests =====
+func TestSaver2_3OtherLicenseSavesText(t *testing.T) {
+ ol := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ LicenseCrossReferences: []string{
+ "http://example.com/License1/",
+ "http://example.com/License1AnotherURL/",
+ },
+ LicenseComment: "this is a license comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`LicenseID: LicenseRef-1
+ExtractedText: <text>License 1 text
+blah blah blah
+blah blah blah blah</text>
+LicenseName: License 1
+LicenseCrossReference: http://example.com/License1/
+LicenseCrossReference: http://example.com/License1AnotherURL/
+LicenseComment: this is a license comment
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderOtherLicense2_3(ol, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3OtherLicenseOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ ol := &v2_3.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`LicenseID: LicenseRef-1
+ExtractedText: <text>License 1 text
+blah blah blah
+blah blah blah blah</text>
+LicenseName: License 1
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderOtherLicense2_3(ol, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_package.go b/tvsaver/saver2v3/save_package.go
new file mode 100644
index 0000000..d323af9
--- /dev/null
+++ b/tvsaver/saver2v3/save_package.go
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderPackage2_3(pkg *v2_3.Package, w io.Writer) error {
+ if pkg.PackageName != "" {
+ fmt.Fprintf(w, "PackageName: %s\n", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != "" {
+ fmt.Fprintf(w, "SPDXID: %s\n", common.RenderElementID(pkg.PackageSPDXIdentifier))
+ }
+ if pkg.PackageVersion != "" {
+ fmt.Fprintf(w, "PackageVersion: %s\n", pkg.PackageVersion)
+ }
+ if pkg.PackageFileName != "" {
+ fmt.Fprintf(w, "PackageFileName: %s\n", pkg.PackageFileName)
+ }
+ if pkg.PackageSupplier != nil && pkg.PackageSupplier.Supplier != "" {
+ if pkg.PackageSupplier.SupplierType == "" {
+ fmt.Fprintf(w, "PackageSupplier: %s\n", pkg.PackageSupplier.Supplier)
+ } else {
+ fmt.Fprintf(w, "PackageSupplier: %s: %s\n", pkg.PackageSupplier.SupplierType, pkg.PackageSupplier.Supplier)
+ }
+ }
+ if pkg.PackageOriginator != nil && pkg.PackageOriginator.Originator != "" {
+ if pkg.PackageOriginator.OriginatorType == "" {
+ fmt.Fprintf(w, "PackageOriginator: %s\n", pkg.PackageOriginator.Originator)
+ } else {
+ fmt.Fprintf(w, "PackageOriginator: %s: %s\n", pkg.PackageOriginator.OriginatorType, pkg.PackageOriginator.Originator)
+ }
+ }
+ if pkg.PackageDownloadLocation != "" {
+ fmt.Fprintf(w, "PackageDownloadLocation: %s\n", pkg.PackageDownloadLocation)
+ }
+ if pkg.PrimaryPackagePurpose != "" {
+ fmt.Fprintf(w, "PrimaryPackagePurpose: %s\n", pkg.PrimaryPackagePurpose)
+ }
+ if pkg.ReleaseDate != "" {
+ fmt.Fprintf(w, "ReleaseDate: %s\n", pkg.ReleaseDate)
+ }
+ if pkg.BuiltDate != "" {
+ fmt.Fprintf(w, "BuiltDate: %s\n", pkg.BuiltDate)
+ }
+ if pkg.ValidUntilDate != "" {
+ fmt.Fprintf(w, "ValidUntilDate: %s\n", pkg.ValidUntilDate)
+ }
+ if pkg.FilesAnalyzed == true {
+ if pkg.IsFilesAnalyzedTagPresent == true {
+ fmt.Fprintf(w, "FilesAnalyzed: true\n")
+ }
+ } else {
+ fmt.Fprintf(w, "FilesAnalyzed: false\n")
+ }
+ if pkg.PackageVerificationCode != nil && pkg.PackageVerificationCode.Value != "" && pkg.FilesAnalyzed == true {
+ if len(pkg.PackageVerificationCode.ExcludedFiles) == 0 {
+ fmt.Fprintf(w, "PackageVerificationCode: %s\n", pkg.PackageVerificationCode.Value)
+ } else {
+ fmt.Fprintf(w, "PackageVerificationCode: %s (excludes: %s)\n", pkg.PackageVerificationCode.Value, strings.Join(pkg.PackageVerificationCode.ExcludedFiles, ", "))
+ }
+ }
+
+ for _, checksum := range pkg.PackageChecksums {
+ fmt.Fprintf(w, "PackageChecksum: %s: %s\n", checksum.Algorithm, checksum.Value)
+ }
+
+ if pkg.PackageHomePage != "" {
+ fmt.Fprintf(w, "PackageHomePage: %s\n", pkg.PackageHomePage)
+ }
+ if pkg.PackageSourceInfo != "" {
+ fmt.Fprintf(w, "PackageSourceInfo: %s\n", textify(pkg.PackageSourceInfo))
+ }
+ if pkg.PackageLicenseConcluded != "" {
+ fmt.Fprintf(w, "PackageLicenseConcluded: %s\n", pkg.PackageLicenseConcluded)
+ }
+ if pkg.FilesAnalyzed == true {
+ for _, s := range pkg.PackageLicenseInfoFromFiles {
+ fmt.Fprintf(w, "PackageLicenseInfoFromFiles: %s\n", s)
+ }
+ }
+ if pkg.PackageLicenseDeclared != "" {
+ fmt.Fprintf(w, "PackageLicenseDeclared: %s\n", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageLicenseComments != "" {
+ fmt.Fprintf(w, "PackageLicenseComments: %s\n", textify(pkg.PackageLicenseComments))
+ }
+ if pkg.PackageCopyrightText != "" {
+ fmt.Fprintf(w, "PackageCopyrightText: %s\n", textify(pkg.PackageCopyrightText))
+ }
+ if pkg.PackageSummary != "" {
+ fmt.Fprintf(w, "PackageSummary: %s\n", textify(pkg.PackageSummary))
+ }
+ if pkg.PackageDescription != "" {
+ fmt.Fprintf(w, "PackageDescription: %s\n", textify(pkg.PackageDescription))
+ }
+ if pkg.PackageComment != "" {
+ fmt.Fprintf(w, "PackageComment: %s\n", textify(pkg.PackageComment))
+ }
+ for _, s := range pkg.PackageExternalReferences {
+ fmt.Fprintf(w, "ExternalRef: %s %s %s\n", s.Category, s.RefType, s.Locator)
+ if s.ExternalRefComment != "" {
+ fmt.Fprintf(w, "ExternalRefComment: %s\n", textify(s.ExternalRefComment))
+ }
+ }
+ for _, s := range pkg.PackageAttributionTexts {
+ fmt.Fprintf(w, "PackageAttributionText: %s\n", textify(s))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ // also render any files for this package
+ sort.Slice(pkg.Files, func(i, j int) bool {
+ return pkg.Files[i].FileSPDXIdentifier < pkg.Files[j].FileSPDXIdentifier
+ })
+ for _, fi := range pkg.Files {
+ renderFile2_3(fi, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_package_test.go b/tvsaver/saver2v3/save_package_test.go
new file mode 100644
index 0000000..435b5b5
--- /dev/null
+++ b/tvsaver/saver2v3/save_package_test.go
@@ -0,0 +1,531 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Package section Saver tests =====
+func TestSaver2_3PackageSavesTextCombo1(t *testing.T) {
+ // include package external refs
+ // test Supplier:Organization, Originator:Person
+ // FilesAnalyzed true, IsFilesAnalyzedTagPresent true
+ // PackageVerificationCodeExcludedFile has string
+
+ // NOTE, this is an entirely made up CPE and the format is likely invalid
+ per1 := &v2_3.PackageExternalReference{
+ Category: "SECURITY",
+ RefType: "cpe22Type",
+ Locator: "cpe:/a:john_doe_inc:p1:0.1.0",
+ ExternalRefComment: "this is an external ref comment #1",
+ }
+
+ // NOTE, this is an entirely made up NPM
+ per2 := &v2_3.PackageExternalReference{
+ Category: "PACKAGE-MANAGER",
+ RefType: "npm",
+ Locator: "p1@0.1.0",
+ ExternalRefComment: `this is a
+multi-line external ref comment`,
+ }
+
+ // NOTE, this is an entirely made up SWH persistent ID
+ per3 := &v2_3.PackageExternalReference{
+ Category: "PERSISTENT-ID",
+ RefType: "swh",
+ Locator: "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2",
+ // no ExternalRefComment for this one
+ }
+
+ per4 := &v2_3.PackageExternalReference{
+ Category: "OTHER",
+ RefType: "anything",
+ Locator: "anything-without-spaces-can-go-here",
+ // no ExternalRefComment for this one
+ }
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageVersion: "0.1.0",
+ PackageFileName: "p1-0.1.0-master.tar.gz",
+ PackageSupplier: &common.Supplier{SupplierType: "Organization", Supplier: "John Doe, Inc."},
+ PackageOriginator: &common.Originator{Originator: "John Doe", OriginatorType: "Person"},
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "0123456789abcdef0123456789abcdef01234567",
+ ExcludedFiles: []string{"p1-0.1.0.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ {
+ Algorithm: common.MD5,
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ PackageHomePage: "http://example.com/p1",
+ PackageSourceInfo: "this is a source comment",
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageLicenseComments: "this is a license comment(s)",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ PackageSummary: "this is a summary comment",
+ PackageDescription: "this is a description comment",
+ PackageComment: "this is a comment comment",
+ PackageAttributionTexts: []string{"Include this notice in all advertising materials"},
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ per1,
+ per2,
+ per3,
+ per4,
+ },
+ PrimaryPackagePurpose: "LIBRARY",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageVersion: 0.1.0
+PackageFileName: p1-0.1.0-master.tar.gz
+PackageSupplier: Organization: John Doe, Inc.
+PackageOriginator: Person: John Doe
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+PrimaryPackagePurpose: LIBRARY
+ReleaseDate: 2021-10-15T02:38:00Z
+BuiltDate: 2021-09-15T02:38:00Z
+ValidUntilDate: 2022-10-15T02:38:00Z
+FilesAnalyzed: true
+PackageVerificationCode: 0123456789abcdef0123456789abcdef01234567 (excludes: p1-0.1.0.spdx)
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageHomePage: http://example.com/p1
+PackageSourceInfo: this is a source comment
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseInfoFromFiles: Apache-1.1
+PackageLicenseInfoFromFiles: Apache-2.0
+PackageLicenseInfoFromFiles: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageLicenseComments: this is a license comment(s)
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+PackageSummary: this is a summary comment
+PackageDescription: this is a description comment
+PackageComment: this is a comment comment
+ExternalRef: SECURITY cpe22Type cpe:/a:john_doe_inc:p1:0.1.0
+ExternalRefComment: this is an external ref comment #1
+ExternalRef: PACKAGE-MANAGER npm p1@0.1.0
+ExternalRefComment: <text>this is a
+multi-line external ref comment</text>
+ExternalRef: PERSISTENT-ID swh swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2
+ExternalRef: OTHER anything anything-without-spaces-can-go-here
+PackageAttributionText: Include this notice in all advertising materials
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSavesTextCombo2(t *testing.T) {
+ // no package external refs
+ // test Supplier:NOASSERTION, Originator:Organization
+ // FilesAnalyzed true, IsFilesAnalyzedTagPresent false
+ // PackageVerificationCodeExcludedFile is empty
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageVersion: "0.1.0",
+ PackageFileName: "p1-0.1.0-master.tar.gz",
+ PackageSupplier: &common.Supplier{Supplier: "NOASSERTION"},
+ PackageOriginator: &common.Originator{OriginatorType: "Organization", Originator: "John Doe, Inc."},
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "0123456789abcdef0123456789abcdef01234567"},
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ {
+ Algorithm: common.MD5,
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ PackageHomePage: "http://example.com/p1",
+ PackageSourceInfo: "this is a source comment",
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageLicenseComments: "this is a license comment(s)",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ PackageSummary: "this is a summary comment",
+ PackageDescription: "this is a description comment",
+ PackageComment: "this is a comment comment",
+ PackageAttributionTexts: []string{"Include this notice in all advertising materials"},
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageVersion: 0.1.0
+PackageFileName: p1-0.1.0-master.tar.gz
+PackageSupplier: NOASSERTION
+PackageOriginator: Organization: John Doe, Inc.
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+PackageVerificationCode: 0123456789abcdef0123456789abcdef01234567
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageHomePage: http://example.com/p1
+PackageSourceInfo: this is a source comment
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseInfoFromFiles: Apache-1.1
+PackageLicenseInfoFromFiles: Apache-2.0
+PackageLicenseInfoFromFiles: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageLicenseComments: this is a license comment(s)
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+PackageSummary: this is a summary comment
+PackageDescription: this is a description comment
+PackageComment: this is a comment comment
+PackageAttributionText: Include this notice in all advertising materials
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSavesTextCombo3(t *testing.T) {
+ // no package external refs
+ // test Supplier:Person, Originator:NOASSERTION
+ // FilesAnalyzed false, IsFilesAnalyzedTagPresent true
+ // PackageVerificationCodeExcludedFile is empty
+ // three PackageAttributionTexts, one with multi-line text
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageVersion: "0.1.0",
+ PackageFileName: "p1-0.1.0-master.tar.gz",
+ PackageSupplier: &common.Supplier{Supplier: "John Doe", SupplierType: "Person"},
+ PackageOriginator: &common.Originator{Originator: "NOASSERTION"},
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ // NOTE that verification code MUST be omitted from output
+ // since FilesAnalyzed is false
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "0123456789abcdef0123456789abcdef01234567"},
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ {
+ Algorithm: common.MD5,
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ PackageHomePage: "http://example.com/p1",
+ PackageSourceInfo: "this is a source comment",
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ // NOTE that license info from files MUST be omitted from output
+ // since FilesAnalyzed is false
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageLicenseComments: "this is a license comment(s)",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ PackageSummary: "this is a summary comment",
+ PackageDescription: "this is a description comment",
+ PackageComment: "this is a comment comment",
+ PackageAttributionTexts: []string{
+ "Include this notice in all advertising materials",
+ "and also this notice",
+ `and this multi-line notice
+which goes across two lines`,
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageVersion: 0.1.0
+PackageFileName: p1-0.1.0-master.tar.gz
+PackageSupplier: Person: John Doe
+PackageOriginator: NOASSERTION
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageHomePage: http://example.com/p1
+PackageSourceInfo: this is a source comment
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageLicenseComments: this is a license comment(s)
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+PackageSummary: this is a summary comment
+PackageDescription: this is a description comment
+PackageComment: this is a comment comment
+PackageAttributionText: Include this notice in all advertising materials
+PackageAttributionText: and also this notice
+PackageAttributionText: <text>and this multi-line notice
+which goes across two lines</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSaveOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ // NOTE that verification code MUST be omitted from output,
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ // NOTE that license info from files MUST be omitted from output
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageSavesFilesIfPresent(t *testing.T) {
+ f1 := &v2_3.File{
+ FileName: "/tmp/whatever1.txt",
+ FileSPDXIdentifier: common.ElementID("File1231"),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileCopyrightText: "Copyright (c) Jane Doe",
+ }
+
+ f2 := &v2_3.File{
+ FileName: "/tmp/whatever2.txt",
+ FileSPDXIdentifier: common.ElementID("File1232"),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983d",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{"MIT"},
+ FileCopyrightText: "Copyright (c) John Doe",
+ }
+
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ // NOTE that verification code MUST be omitted from output,
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ // NOTE that license info from files MUST be omitted from output
+ // even if present in model, since FilesAnalyzed is false
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: "Copyright (c) John Doe, Inc.",
+ Files: []*v2_3.File{
+ f1,
+ f2,
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: Copyright (c) John Doe, Inc.
+
+FileName: /tmp/whatever1.txt
+SPDXID: SPDXRef-File1231
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright (c) Jane Doe
+
+FileName: /tmp/whatever2.txt
+SPDXID: SPDXRef-File1232
+FileChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983d
+LicenseConcluded: MIT
+LicenseInfoInFile: MIT
+FileCopyrightText: Copyright (c) John Doe
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3PackageWrapsMultiLine(t *testing.T) {
+ pkg := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "http://example.com/p1/p1-0.1.0-master.tar.gz",
+ FilesAnalyzed: false,
+ IsFilesAnalyzedTagPresent: true,
+ PackageLicenseConcluded: "GPL-2.0-or-later",
+ PackageLicenseInfoFromFiles: []string{
+ "Apache-1.1",
+ "Apache-2.0",
+ "GPL-2.0-or-later",
+ },
+ PackageLicenseDeclared: "Apache-2.0 OR GPL-2.0-or-later",
+ PackageCopyrightText: `Copyright (c) John Doe, Inc.
+Copyright Jane Doe`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`PackageName: p1
+SPDXID: SPDXRef-p1
+PackageDownloadLocation: http://example.com/p1/p1-0.1.0-master.tar.gz
+FilesAnalyzed: false
+PackageLicenseConcluded: GPL-2.0-or-later
+PackageLicenseDeclared: Apache-2.0 OR GPL-2.0-or-later
+PackageCopyrightText: <text>Copyright (c) John Doe, Inc.
+Copyright Jane Doe</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_relationship.go b/tvsaver/saver2v3/save_relationship.go
new file mode 100644
index 0000000..c83310f
--- /dev/null
+++ b/tvsaver/saver2v3/save_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderRelationship2_3(rln *v2_3.Relationship, w io.Writer) error {
+ rlnAStr := common.RenderDocElementID(rln.RefA)
+ rlnBStr := common.RenderDocElementID(rln.RefB)
+ if rlnAStr != "SPDXRef-" && rlnBStr != "SPDXRef-" && rln.Relationship != "" {
+ fmt.Fprintf(w, "Relationship: %s %s %s\n", rlnAStr, rln.Relationship, rlnBStr)
+ }
+ if rln.RelationshipComment != "" {
+ fmt.Fprintf(w, "RelationshipComment: %s\n", textify(rln.RelationshipComment))
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_relationship_test.go b/tvsaver/saver2v3/save_relationship_test.go
new file mode 100644
index 0000000..26ce0c3
--- /dev/null
+++ b/tvsaver/saver2v3/save_relationship_test.go
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Relationship section Saver tests =====
+func TestSaver2_3RelationshipSavesText(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "2"),
+ Relationship: "DESCRIBES",
+ RelationshipComment: "this is a comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-2
+RelationshipComment: this is a comment
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "2"),
+ Relationship: "DESCRIBES",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString("Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-2\n")
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipCanHaveNONEOnRight(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "PackageA"),
+ RefB: common.MakeDocElementSpecial("NONE"),
+ Relationship: "DEPENDS_ON",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString("Relationship: SPDXRef-PackageA DEPENDS_ON NONE\n")
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipCanHaveNOASSERTIONOnRight(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "PackageA"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "DEPENDS_ON",
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString("Relationship: SPDXRef-PackageA DEPENDS_ON NOASSERTION\n")
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3RelationshipWrapsCommentMultiLine(t *testing.T) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "2"),
+ Relationship: "DESCRIBES",
+ RelationshipComment: `this is a
+multi-line comment`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ // no trailing blank newline
+ want := bytes.NewBufferString(`Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-2
+RelationshipComment: <text>this is a
+multi-line comment</text>
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderRelationship2_3(rln, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_review.go b/tvsaver/saver2v3/save_review.go
new file mode 100644
index 0000000..72bac11
--- /dev/null
+++ b/tvsaver/saver2v3/save_review.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderReview2_3(rev *v2_3.Review, w io.Writer) error {
+ if rev.Reviewer != "" && rev.ReviewerType != "" {
+ fmt.Fprintf(w, "Reviewer: %s: %s\n", rev.ReviewerType, rev.Reviewer)
+ }
+ if rev.ReviewDate != "" {
+ fmt.Fprintf(w, "ReviewDate: %s\n", rev.ReviewDate)
+ }
+ if rev.ReviewComment != "" {
+ fmt.Fprintf(w, "ReviewComment: %s\n", textify(rev.ReviewComment))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_review_test.go b/tvsaver/saver2v3/save_review_test.go
new file mode 100644
index 0000000..5eec3bc
--- /dev/null
+++ b/tvsaver/saver2v3/save_review_test.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Review section Saver tests =====
+func TestSaver2_3ReviewSavesText(t *testing.T) {
+ rev := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ ReviewComment: "this is a review comment",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+ReviewComment: this is a review comment
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderReview2_3(rev, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3ReviewOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rev := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderReview2_3(rev, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3ReviewWrapsMultiLine(t *testing.T) {
+ rev := &v2_3.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ ReviewComment: `this is a
+multi-line review comment`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`Reviewer: Person: John Doe
+ReviewDate: 2018-10-14T10:28:00Z
+ReviewComment: <text>this is a
+multi-line review comment</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderReview2_3(rev, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/save_snippet.go b/tvsaver/saver2v3/save_snippet.go
new file mode 100644
index 0000000..2ba4a3b
--- /dev/null
+++ b/tvsaver/saver2v3/save_snippet.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func renderSnippet2_3(sn *v2_3.Snippet, w io.Writer) error {
+ if sn.SnippetSPDXIdentifier != "" {
+ fmt.Fprintf(w, "SnippetSPDXID: %s\n", common.RenderElementID(sn.SnippetSPDXIdentifier))
+ }
+ snFromFileIDStr := common.RenderElementID(sn.SnippetFromFileSPDXIdentifier)
+ if snFromFileIDStr != "" {
+ fmt.Fprintf(w, "SnippetFromFileSPDXID: %s\n", snFromFileIDStr)
+ }
+
+ for _, snippetRange := range sn.Ranges {
+ if snippetRange.StartPointer.Offset != 0 && snippetRange.EndPointer.Offset != 0 {
+ fmt.Fprintf(w, "SnippetByteRange: %d:%d\n", snippetRange.StartPointer.Offset, snippetRange.EndPointer.Offset)
+ }
+ if snippetRange.StartPointer.LineNumber != 0 && snippetRange.EndPointer.LineNumber != 0 {
+ fmt.Fprintf(w, "SnippetLineRange: %d:%d\n", snippetRange.StartPointer.LineNumber, snippetRange.EndPointer.LineNumber)
+ }
+ }
+ if sn.SnippetLicenseConcluded != "" {
+ fmt.Fprintf(w, "SnippetLicenseConcluded: %s\n", sn.SnippetLicenseConcluded)
+ }
+ for _, s := range sn.LicenseInfoInSnippet {
+ fmt.Fprintf(w, "LicenseInfoInSnippet: %s\n", s)
+ }
+ if sn.SnippetLicenseComments != "" {
+ fmt.Fprintf(w, "SnippetLicenseComments: %s\n", textify(sn.SnippetLicenseComments))
+ }
+ if sn.SnippetCopyrightText != "" {
+ fmt.Fprintf(w, "SnippetCopyrightText: %s\n", textify(sn.SnippetCopyrightText))
+ }
+ if sn.SnippetComment != "" {
+ fmt.Fprintf(w, "SnippetComment: %s\n", textify(sn.SnippetComment))
+ }
+ if sn.SnippetName != "" {
+ fmt.Fprintf(w, "SnippetName: %s\n", sn.SnippetName)
+ }
+ for _, s := range sn.SnippetAttributionTexts {
+ fmt.Fprintf(w, "SnippetAttributionText: %s\n", textify(s))
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v3/save_snippet_test.go b/tvsaver/saver2v3/save_snippet_test.go
new file mode 100644
index 0000000..ef10194
--- /dev/null
+++ b/tvsaver/saver2v3/save_snippet_test.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== Snippet section Saver tests =====
+func TestSaver2_3SnippetSavesText(t *testing.T) {
+ sn := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet17"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File292").ElementRefID,
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{LineNumber: 3},
+ EndPointer: common.SnippetRangePointer{LineNumber: 8},
+ },
+ {
+ StartPointer: common.SnippetRangePointer{Offset: 17},
+ EndPointer: common.SnippetRangePointer{Offset: 209},
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInSnippet: []string{
+ "GPL-2.0-or-later",
+ "MIT",
+ },
+ SnippetLicenseComments: "this is a comment(s) about the snippet license",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ SnippetComment: "this is a snippet comment",
+ SnippetName: "from John's program",
+ SnippetAttributionTexts: []string{"some attributions"},
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`SnippetSPDXID: SPDXRef-Snippet17
+SnippetFromFileSPDXID: SPDXRef-File292
+SnippetLineRange: 3:8
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+LicenseInfoInSnippet: GPL-2.0-or-later
+LicenseInfoInSnippet: MIT
+SnippetLicenseComments: this is a comment(s) about the snippet license
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+SnippetComment: this is a snippet comment
+SnippetName: from John's program
+SnippetAttributionText: some attributions
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_3(sn, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3SnippetOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ sn := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet17"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File292").ElementRefID,
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{Offset: 17},
+ EndPointer: common.SnippetRangePointer{Offset: 209},
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: "Copyright (c) John Doe 20x6",
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`SnippetSPDXID: SPDXRef-Snippet17
+SnippetFromFileSPDXID: SPDXRef-File292
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: Copyright (c) John Doe 20x6
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_3(sn, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func TestSaver2_3SnippetWrapsCopyrightMultiline(t *testing.T) {
+ sn := &v2_3.Snippet{
+ SnippetSPDXIdentifier: common.ElementID("Snippet17"),
+ SnippetFromFileSPDXIdentifier: common.MakeDocElementID("", "File292").ElementRefID,
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{Offset: 17},
+ EndPointer: common.SnippetRangePointer{Offset: 209},
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-or-later",
+ SnippetCopyrightText: `Copyright (c) John Doe 20x6
+Copyright (c) John Doe 20x6`,
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(`SnippetSPDXID: SPDXRef-Snippet17
+SnippetFromFileSPDXID: SPDXRef-File292
+SnippetByteRange: 17:209
+SnippetLicenseConcluded: GPL-2.0-or-later
+SnippetCopyrightText: <text>Copyright (c) John Doe 20x6
+Copyright (c) John Doe 20x6</text>
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_3(sn, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
diff --git a/tvsaver/saver2v3/util.go b/tvsaver/saver2v3/util.go
new file mode 100644
index 0000000..4dec724
--- /dev/null
+++ b/tvsaver/saver2v3/util.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "fmt"
+ "strings"
+)
+
+func textify(s string) string {
+ if strings.Contains(s, "\n") {
+ return fmt.Sprintf("<text>%s</text>", s)
+ }
+
+ return s
+}
diff --git a/tvsaver/saver2v3/util_test.go b/tvsaver/saver2v3/util_test.go
new file mode 100644
index 0000000..b4b7553
--- /dev/null
+++ b/tvsaver/saver2v3/util_test.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v3
+
+import (
+ "testing"
+)
+
+// ===== Utility function tests =====
+func TestTextifyWrapsStringWithNewline(t *testing.T) {
+ s := `this text has
+a newline in it`
+ want := `<text>this text has
+a newline in it</text>`
+
+ got := textify(s)
+
+ if want != got {
+ t.Errorf("Expected %s, got %s", want, got)
+ }
+}
+
+func TestTextifyDoesNotWrapsStringWithNoNewline(t *testing.T) {
+ s := `this text has no newline in it`
+ want := s
+
+ got := textify(s)
+
+ if want != got {
+ t.Errorf("Expected %s, got %s", want, got)
+ }
+}
diff --git a/tvsaver/tvsaver.go b/tvsaver/tvsaver.go
index 09d16dc..a73b4b9 100644
--- a/tvsaver/tvsaver.go
+++ b/tvsaver/tvsaver.go
@@ -8,8 +8,10 @@ import (
"github.com/spdx/tools-golang/spdx/v2_1"
"github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
"github.com/spdx/tools-golang/tvsaver/saver2v1"
"github.com/spdx/tools-golang/tvsaver/saver2v2"
+ "github.com/spdx/tools-golang/tvsaver/saver2v3"
)
// Save2_1 takes an io.Writer and an SPDX Document (version 2.1),
@@ -25,3 +27,10 @@ func Save2_1(doc *v2_1.Document, w io.Writer) error {
func Save2_2(doc *v2_2.Document, w io.Writer) error {
return saver2v2.RenderDocument2_2(doc, w)
}
+
+// Save2_3 takes an io.Writer and an SPDX Document (version 2.3),
+// and writes it to the writer in tag-value format. It returns error
+// if any error is encountered.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ return saver2v3.RenderDocument2_3(doc, w)
+}