aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSadaf Ebrahimi <sadafebrahimi@google.com>2023-01-26 18:29:15 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-01-26 18:29:15 +0000
commitb925feafab4bd54a2d608e4621a5210bc67d67c6 (patch)
tree6ea4841a079772cb06df5ec2cf99637f8d53805b
parent5f73058e15df15ab2a35ca0c9d506b4fd0a83de7 (diff)
parent8dba382b59616b372f0ee6f95fe66c942bb790e0 (diff)
downloadspdx-tools-b925feafab4bd54a2d608e4621a5210bc67d67c6.tar.gz
Upgrade spdx-tools to v0.4.0 am: 75f88b4895 am: f12cf92582 am: 8dba382b59
Original change: https://android-review.googlesource.com/c/platform/external/spdx-tools/+/2401868 Change-Id: I14274d30850f3af3184815b3a3820b1f744d0a1f Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.github/workflows/build.yml18
-rw-r--r--.gitignore8
-rw-r--r--CONTRIBUTING.md71
-rw-r--r--LICENSE.code550
-rw-r--r--LICENSE.docs399
-rw-r--r--MAINTAINERS6
-rw-r--r--METADATA19
-rw-r--r--README.md87
-rw-r--r--RELEASE-NOTES.md102
-rw-r--r--SECURITY.md3
-rw-r--r--builder/build.go224
-rw-r--r--builder/build_test.go826
-rw-r--r--builder/builder2v1/build_creation_info.go44
-rw-r--r--builder/builder2v1/build_creation_info_test.go111
-rw-r--r--builder/builder2v1/build_file.go56
-rw-r--r--builder/builder2v1/build_file_test.go74
-rw-r--r--builder/builder2v1/build_package.go78
-rw-r--r--builder/builder2v1/build_package_test.go156
-rw-r--r--builder/builder2v1/build_relationship.go24
-rw-r--r--builder/builder2v1/build_relationship_test.go33
-rw-r--r--builder/builder2v2/build_creation_info.go47
-rw-r--r--builder/builder2v2/build_creation_info_test.go111
-rw-r--r--builder/builder2v2/build_file.go56
-rw-r--r--builder/builder2v2/build_file_test.go74
-rw-r--r--builder/builder2v2/build_package.go79
-rw-r--r--builder/builder2v2/build_package_test.go156
-rw-r--r--builder/builder2v2/build_relationship.go24
-rw-r--r--builder/builder2v2/build_relationship_test.go33
-rw-r--r--builder/builder2v3/build_creation_info.go47
-rw-r--r--builder/builder2v3/build_creation_info_test.go111
-rw-r--r--builder/builder2v3/build_file.go56
-rw-r--r--builder/builder2v3/build_file_test.go74
-rw-r--r--builder/builder2v3/build_package.go79
-rw-r--r--builder/builder2v3/build_package_test.go156
-rw-r--r--builder/builder2v3/build_relationship.go24
-rw-r--r--builder/builder2v3/build_relationship_test.go33
-rw-r--r--docs/builder-assumptions.md6
-rw-r--r--docs/idsearcher-assumptions.md13
-rw-r--r--docs/jsonloader.md23
-rw-r--r--docs/jsonsaver.md28
-rw-r--r--docs/licensediff-assumptions.md7
-rw-r--r--docs/tvloader-assumptions.md34
-rw-r--r--docs/tvsaver-assumptions.md14
-rw-r--r--examples/1-load/example_load.go116
-rw-r--r--examples/10-jsonloader/example_json_loader.go55
-rw-r--r--examples/11-yamltotv/exampleyamltotv.go69
-rw-r--r--examples/12-tvtoyaml/exampletvtoyaml.go68
-rw-r--r--examples/13-yamlloader/exampleYAMLLoader.go55
-rw-r--r--examples/2-load-save/example_load_save.go69
-rw-r--r--examples/3-build/example_build.go118
-rw-r--r--examples/4-search/example_search.go149
-rw-r--r--examples/5-report/example_report.go99
-rw-r--r--examples/6-licensediff/example_licensediff.go169
-rw-r--r--examples/7-rdfloader/exampleRDFLoader.go49
-rw-r--r--examples/8-jsontotv/examplejsontotv.go68
-rw-r--r--examples/9-tvtojson/exampletvtojson.go68
-rw-r--r--examples/README.md120
-rw-r--r--examples/sample-docs/json/SPDXJSONExample-v2.2.spdx.json284
-rw-r--r--examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json406
-rw-r--r--examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf1365
-rw-r--r--examples/sample-docs/tv/SPDXTagExample-v2.2.spdx329
-rw-r--r--examples/sample-docs/tv/SPDXTagExample-v2.3.spdx328
-rw-r--r--examples/sample-docs/tv/hello-modified.spdx57
-rw-r--r--examples/sample-docs/tv/hello.spdx57
-rw-r--r--examples/sample-docs/xls/SPDXSpreadsheetExample-v2.2.xlsxbin0 -> 14949 bytes
-rw-r--r--examples/sample-docs/xml/SPDXXMLExample-v2.2.spdx.xml443
-rw-r--r--examples/sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml390
-rw-r--r--examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml412
-rw-r--r--go.mod9
-rw-r--r--go.sum14
-rw-r--r--idsearcher/idsearcher.go483
-rw-r--r--idsearcher/idsearcher_test.go1187
-rw-r--r--json/json_v2_2_test.go451
-rw-r--r--json/json_v2_3_test.go490
-rw-r--r--json/parser.go48
-rw-r--r--json/writer.go41
-rw-r--r--licensediff/licensediff.go139
-rw-r--r--licensediff/licensediff_test.go1606
-rw-r--r--rdfloader/parser2v2/constants.go264
-rw-r--r--rdfloader/parser2v2/license_utils.go119
-rw-r--r--rdfloader/parser2v2/license_utils_test.go345
-rw-r--r--rdfloader/parser2v2/parse_annotation.go81
-rw-r--r--rdfloader/parser2v2/parse_annotation_test.go183
-rw-r--r--rdfloader/parser2v2/parse_creation_info.go58
-rw-r--r--rdfloader/parser2v2/parse_creation_info_test.go107
-rw-r--r--rdfloader/parser2v2/parse_file.go215
-rw-r--r--rdfloader/parser2v2/parse_file_test.go765
-rw-r--r--rdfloader/parser2v2/parse_license.go291
-rw-r--r--rdfloader/parser2v2/parse_license_test.go853
-rw-r--r--rdfloader/parser2v2/parse_other_license_info.go38
-rw-r--r--rdfloader/parser2v2/parse_other_license_info_test.go74
-rw-r--r--rdfloader/parser2v2/parse_package.go345
-rw-r--r--rdfloader/parser2v2/parse_package_test.go768
-rw-r--r--rdfloader/parser2v2/parse_relationship.go156
-rw-r--r--rdfloader/parser2v2/parse_relationship_test.go397
-rw-r--r--rdfloader/parser2v2/parse_review.go38
-rw-r--r--rdfloader/parser2v2/parse_review_test.go73
-rw-r--r--rdfloader/parser2v2/parse_snippet_info.go199
-rw-r--r--rdfloader/parser2v2/parse_snippet_info_test.go536
-rw-r--r--rdfloader/parser2v2/parse_spdx_document.go120
-rw-r--r--rdfloader/parser2v2/parse_spdx_document_test.go241
-rw-r--r--rdfloader/parser2v2/parser.go133
-rw-r--r--rdfloader/parser2v2/parser_test.go172
-rw-r--r--rdfloader/parser2v2/types.go127
-rw-r--r--rdfloader/parser2v2/utils.go155
-rw-r--r--rdfloader/parser2v2/utils_test.go290
-rw-r--r--rdfloader/parser2v3/constants.go268
-rw-r--r--rdfloader/parser2v3/license_utils.go119
-rw-r--r--rdfloader/parser2v3/license_utils_test.go345
-rw-r--r--rdfloader/parser2v3/parse_annotation.go81
-rw-r--r--rdfloader/parser2v3/parse_annotation_test.go183
-rw-r--r--rdfloader/parser2v3/parse_creation_info.go58
-rw-r--r--rdfloader/parser2v3/parse_creation_info_test.go107
-rw-r--r--rdfloader/parser2v3/parse_file.go223
-rw-r--r--rdfloader/parser2v3/parse_file_test.go765
-rw-r--r--rdfloader/parser2v3/parse_license.go291
-rw-r--r--rdfloader/parser2v3/parse_license_test.go853
-rw-r--r--rdfloader/parser2v3/parse_other_license_info.go38
-rw-r--r--rdfloader/parser2v3/parse_other_license_info_test.go74
-rw-r--r--rdfloader/parser2v3/parse_package.go380
-rw-r--r--rdfloader/parser2v3/parse_package_test.go788
-rw-r--r--rdfloader/parser2v3/parse_relationship.go156
-rw-r--r--rdfloader/parser2v3/parse_relationship_test.go397
-rw-r--r--rdfloader/parser2v3/parse_review.go38
-rw-r--r--rdfloader/parser2v3/parse_review_test.go73
-rw-r--r--rdfloader/parser2v3/parse_snippet_info.go199
-rw-r--r--rdfloader/parser2v3/parse_snippet_info_test.go536
-rw-r--r--rdfloader/parser2v3/parse_spdx_document.go120
-rw-r--r--rdfloader/parser2v3/parse_spdx_document_test.go241
-rw-r--r--rdfloader/parser2v3/parser.go133
-rw-r--r--rdfloader/parser2v3/parser_test.go172
-rw-r--r--rdfloader/parser2v3/types.go127
-rw-r--r--rdfloader/parser2v3/utils.go155
-rw-r--r--rdfloader/parser2v3/utils_test.go290
-rw-r--r--rdfloader/rdfloader.go37
-rw-r--r--rdfloader/rdfloader_test.go118
-rw-r--r--reporter/reporter.go205
-rw-r--r--reporter/reporter_test.go384
-rw-r--r--spdx/common/annotation.go44
-rw-r--r--spdx/common/checksum.go34
-rw-r--r--spdx/common/creation_info.go44
-rw-r--r--spdx/common/external.go67
-rw-r--r--spdx/common/identifier.go173
-rw-r--r--spdx/common/identifier_test.go314
-rw-r--r--spdx/common/package.go105
-rw-r--r--spdx/common/snippet.go20
-rw-r--r--spdx/v2_1/annotation.go29
-rw-r--r--spdx/v2_1/creation_info.go26
-rw-r--r--spdx/v2_1/document.go65
-rw-r--r--spdx/v2_1/file.go90
-rw-r--r--spdx/v2_1/other_license.go31
-rw-r--r--spdx/v2_1/package.go120
-rw-r--r--spdx/v2_1/relationship.go23
-rw-r--r--spdx/v2_1/review.go25
-rw-r--r--spdx/v2_1/snippet.go44
-rw-r--r--spdx/v2_2/annotation.go29
-rw-r--r--spdx/v2_2/creation_info.go26
-rw-r--r--spdx/v2_2/document.go65
-rw-r--r--spdx/v2_2/file.go94
-rw-r--r--spdx/v2_2/other_license.go31
-rw-r--r--spdx/v2_2/package.go133
-rw-r--r--spdx/v2_2/relationship.go23
-rw-r--r--spdx/v2_2/review.go25
-rw-r--r--spdx/v2_2/snippet.go48
-rw-r--r--spdx/v2_3/annotation.go29
-rw-r--r--spdx/v2_3/creation_info.go26
-rw-r--r--spdx/v2_3/document.go65
-rw-r--r--spdx/v2_3/file.go96
-rw-r--r--spdx/v2_3/other_license.go31
-rw-r--r--spdx/v2_3/package.go151
-rw-r--r--spdx/v2_3/relationship.go23
-rw-r--r--spdx/v2_3/review.go25
-rw-r--r--spdx/v2_3/snippet.go48
-rw-r--r--spdxlib/described_elements.go166
-rw-r--r--spdxlib/described_elements_test.go519
-rw-r--r--spdxlib/documents.go101
-rw-r--r--spdxlib/documents_test.go270
-rw-r--r--spdxlib/element_ids.go18
-rw-r--r--spdxlib/element_ids_test.go19
-rw-r--r--spdxlib/relationships.go52
-rw-r--r--spdxlib/relationships_test.go144
-rw-r--r--test/v2_3/tv_test.go491
-rw-r--r--testdata/project1/emptyfile.testdata.txt0
-rw-r--r--testdata/project1/file1.testdata.txt1
-rw-r--r--testdata/project1/file3.testdata.txt1
-rw-r--r--testdata/project1/folder1/file4.testdata.txt2
-rw-r--r--testdata/project1/lastfile.testdata.txt3
l---------testdata/project1/symbolic-link1
-rw-r--r--testdata/project2/folder/has-one-id.py5
-rw-r--r--testdata/project2/folder/has-trailing-comment-marker.c8
-rw-r--r--testdata/project2/has-duplicate-ids.txt7
-rw-r--r--testdata/project2/has-id.txt3
-rw-r--r--testdata/project2/has-multiple-ids.txt11
-rw-r--r--testdata/project2/no-id.txt1
-rw-r--r--testdata/project3/alsoEXCLUDEthis.txt0
-rw-r--r--testdata/project3/dontscan.txt1
-rw-r--r--testdata/project3/excludedir/no.txt0
-rw-r--r--testdata/project3/ignoredir/notThisOne.txt0
-rw-r--r--testdata/project3/ignorefile.txt0
-rw-r--r--testdata/project3/keep.txt0
-rw-r--r--testdata/project3/keep/keep.txt1
-rw-r--r--testdata/project3/subdir/ignoredir/notThisOneEither.txt0
-rw-r--r--testdata/project3/subdir/ignorefile.txt0
-rw-r--r--testdata/project3/subdir/keep/dontscan.txt1
-rw-r--r--testdata/project3/subdir/keep/keep.txt1
-rw-r--r--testdata/project4/has-id-to-ignore.txt8
-rw-r--r--testdata/project4/has-id.txt3
-rw-r--r--testdata/project4/has-mix-of-ids.txt12
-rw-r--r--tvloader/parser2v1/parse_annotation.go43
-rw-r--r--tvloader/parser2v1/parse_annotation_test.go158
-rw-r--r--tvloader/parser2v1/parse_creation_info.go156
-rw-r--r--tvloader/parser2v1/parse_creation_info_test.go427
-rw-r--r--tvloader/parser2v1/parse_file.go139
-rw-r--r--tvloader/parser2v1/parse_file_test.go938
-rw-r--r--tvloader/parser2v1/parse_other_license.go55
-rw-r--r--tvloader/parser2v1/parse_other_license_test.go339
-rw-r--r--tvloader/parser2v1/parse_package.go218
-rw-r--r--tvloader/parser2v1/parse_package_test.go1097
-rw-r--r--tvloader/parser2v1/parse_relationship.go52
-rw-r--r--tvloader/parser2v1/parse_relationship_test.go171
-rw-r--r--tvloader/parser2v1/parse_review.go63
-rw-r--r--tvloader/parser2v1/parse_review_test.go414
-rw-r--r--tvloader/parser2v1/parse_snippet.go135
-rw-r--r--tvloader/parser2v1/parse_snippet_test.go607
-rw-r--r--tvloader/parser2v1/parser.go103
-rw-r--r--tvloader/parser2v1/parser_test.go97
-rw-r--r--tvloader/parser2v1/types.go57
-rw-r--r--tvloader/parser2v1/util.go98
-rw-r--r--tvloader/parser2v1/util_test.go111
-rw-r--r--tvloader/parser2v2/parse_annotation.go43
-rw-r--r--tvloader/parser2v2/parse_annotation_test.go158
-rw-r--r--tvloader/parser2v2/parse_creation_info.go156
-rw-r--r--tvloader/parser2v2/parse_creation_info_test.go427
-rw-r--r--tvloader/parser2v2/parse_file.go149
-rw-r--r--tvloader/parser2v2/parse_file_test.go954
-rw-r--r--tvloader/parser2v2/parse_other_license.go55
-rw-r--r--tvloader/parser2v2/parse_other_license_test.go339
-rw-r--r--tvloader/parser2v2/parse_package.go232
-rw-r--r--tvloader/parser2v2/parse_package_test.go1118
-rw-r--r--tvloader/parser2v2/parse_relationship.go54
-rw-r--r--tvloader/parser2v2/parse_relationship_test.go202
-rw-r--r--tvloader/parser2v2/parse_review.go63
-rw-r--r--tvloader/parser2v2/parse_review_test.go414
-rw-r--r--tvloader/parser2v2/parse_snippet.go137
-rw-r--r--tvloader/parser2v2/parse_snippet_test.go635
-rw-r--r--tvloader/parser2v2/parser.go100
-rw-r--r--tvloader/parser2v2/parser_test.go97
-rw-r--r--tvloader/parser2v2/types.go57
-rw-r--r--tvloader/parser2v2/util.go115
-rw-r--r--tvloader/parser2v2/util_test.go156
-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.go157
-rw-r--r--tvloader/parser2v3/parse_file_test.go955
-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.go248
-rw-r--r--tvloader/parser2v3/parse_package_test.go1119
-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/reader/reader.go161
-rw-r--r--tvloader/reader/reader_test.go563
-rw-r--r--tvloader/tvloader.go64
-rw-r--r--tvsaver/saver2v1/save_annotation.go32
-rw-r--r--tvsaver/saver2v1/save_annotation_test.go110
-rw-r--r--tvsaver/saver2v1/save_creation_info.go30
-rw-r--r--tvsaver/saver2v1/save_creation_info_test.go112
-rw-r--r--tvsaver/saver2v1/save_document.go104
-rw-r--r--tvsaver/saver2v1/save_document_test.go343
-rw-r--r--tvsaver/saver2v1/save_file.go77
-rw-r--r--tvsaver/saver2v1/save_file_test.go306
-rw-r--r--tvsaver/saver2v1/save_other_license.go32
-rw-r--r--tvsaver/saver2v1/save_other_license_test.go83
-rw-r--r--tvsaver/saver2v1/save_package.go114
-rw-r--r--tvsaver/saver2v1/save_package_test.go498
-rw-r--r--tvsaver/saver2v1/save_relationship.go24
-rw-r--r--tvsaver/saver2v1/save_relationship_test.go95
-rw-r--r--tvsaver/saver2v1/save_review.go26
-rw-r--r--tvsaver/saver2v1/save_review_test.go98
-rw-r--r--tvsaver/saver2v1/save_snippet.go52
-rw-r--r--tvsaver/saver2v1/save_snippet_test.go142
-rw-r--r--tvsaver/saver2v1/util.go16
-rw-r--r--tvsaver/saver2v1/util_test.go32
-rw-r--r--tvsaver/saver2v2/save_annotation.go32
-rw-r--r--tvsaver/saver2v2/save_annotation_test.go110
-rw-r--r--tvsaver/saver2v2/save_creation_info.go30
-rw-r--r--tvsaver/saver2v2/save_creation_info_test.go112
-rw-r--r--tvsaver/saver2v2/save_document.go104
-rw-r--r--tvsaver/saver2v2/save_document_test.go343
-rw-r--r--tvsaver/saver2v2/save_file.go81
-rw-r--r--tvsaver/saver2v2/save_file_test.go314
-rw-r--r--tvsaver/saver2v2/save_other_license.go32
-rw-r--r--tvsaver/saver2v2/save_other_license_test.go83
-rw-r--r--tvsaver/saver2v2/save_package.go117
-rw-r--r--tvsaver/saver2v2/save_package_test.go523
-rw-r--r--tvsaver/saver2v2/save_relationship.go24
-rw-r--r--tvsaver/saver2v2/save_relationship_test.go145
-rw-r--r--tvsaver/saver2v2/save_review.go26
-rw-r--r--tvsaver/saver2v2/save_review_test.go98
-rw-r--r--tvsaver/saver2v2/save_snippet.go55
-rw-r--r--tvsaver/saver2v2/save_snippet_test.go144
-rw-r--r--tvsaver/saver2v2/util.go16
-rw-r--r--tvsaver/saver2v2/util_test.go32
-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.go36
-rw-r--r--utils/filesystem.go124
-rw-r--r--utils/filesystem_test.go226
-rw-r--r--utils/verification.go133
-rw-r--r--utils/verification_test.go444
-rw-r--r--yaml/parser.go48
-rw-r--r--yaml/writer.go41
-rw-r--r--yaml/yaml_v2_2_test.go451
-rw-r--r--yaml/yaml_v2_3_test.go492
343 files changed, 59951 insertions, 0 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..4bce179
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+name: build
+on: [push, pull_request]
+jobs:
+ tests:
+ runs-on: ubuntu-20.04
+ steps:
+ - uses: actions/checkout@main
+ - uses: actions/setup-go@v2
+ with:
+ go-version: '1.18'
+ - name: Run tests
+ run: go test -v -covermode=count -coverprofile=profile.cov ./...
+ - name: Send coverage report to coveralls
+ uses: shogo82148/actions-goveralls@v1
+ with:
+ path-to-profile: profile.cov
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..38da0f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+.vscode
+.idea
+.DS_Store
+scratch/*
+*.swp
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..1b4fbad
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,71 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+# Contributing
+
+All contributions must include a "Signed-off-by" line in the commit message.
+
+This indicates that the contribution is made pursuant to the [Developer Certificate of Origin (DCO)](https://developercertificate.org/), a copy of which is included below.
+
+## Test coverage
+
+Since this library is intended to be relied upon by other tools to work with SPDX data, we are aiming to ensure that it is and remains well-tested.
+
+PRs with new code should include corresponding test files, and should continue to pass existing tests. Unit tests for `foo.go` should be placed in `foo_test.go`. Test data files and folders should be placed in the top-level `testdata/` folder.
+
+To run the test suite, from the top-level directory run: `go test ./...`
+
+## License information
+
+New **code files** should include a [short-form SPDX ID](https://spdx.org/ids) at the top, indicating the project license for code, which is Apache-2.0 OR GPL-2.0-or-later. This should look like the following:
+
+```
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+```
+
+New **documentation files** should include a [short-form SPDX ID](https://spdx.org/ids) at the top, indicating the project license for documentation, which is CC-BY-4.0. This should look like the following:
+
+```
+SPDX-License-Identifier: CC-BY-4.0
+```
+
+## Developer Certificate of Origin (DCO)
+
+```
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+```
diff --git a/LICENSE.code b/LICENSE.code
new file mode 100644
index 0000000..07efb62
--- /dev/null
+++ b/LICENSE.code
@@ -0,0 +1,550 @@
+The tools-golang source code is provided and may be used, at your option,
+under either:
+* Apache License, version 2.0 (Apache-2.0), OR
+* GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
+
+Copies of both licenses are included below.
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/LICENSE.docs b/LICENSE.docs
new file mode 100644
index 0000000..2c8e93c
--- /dev/null
+++ b/LICENSE.docs
@@ -0,0 +1,399 @@
+The tools-golang documentation is provided under the Creative Commons Attribution
+4.0 International license (CC-BY-4.0), a copy of which is provided below.
+
+Attribution 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution 4.0 International Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution 4.0 International Public License ("Public License"). To the
+extent this Public License may be interpreted as a contract, You are
+granted the Licensed Rights in consideration of Your acceptance of
+these terms and conditions, and the Licensor grants You such rights in
+consideration of benefits the Licensor receives from making the
+Licensed Material available under these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ d. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ e. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ f. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ g. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ h. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ i. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ j. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ k. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ 4. If You Share Adapted Material You produce, the Adapter's
+ License You apply must not prevent recipients of the Adapted
+ Material from complying with this Public License.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material; and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.
+
diff --git a/MAINTAINERS b/MAINTAINERS
new file mode 100644
index 0000000..c672ec8
--- /dev/null
+++ b/MAINTAINERS
@@ -0,0 +1,6 @@
+# spdx/tools-golang project maintainers
+#
+# GitHub ID, Name, Affiliation, Email address
+swinslow, Steve Winslow, NA, steve@swinslow.net
+RishabhBhatnagar, Rishabh Bhatnagar, media.net, bhatnagarrishabh4@gmail.com
+lumjjb, Brandon Lum, Google, lumjjb@gmail.com
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..0c02cec
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,19 @@
+# This project was upgraded with external_updater.
+# Usage: tools/external_updater/updater.sh update spdx-tools
+# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md
+
+name: "spdx-tools"
+description: "tools-golang is a collection of Go packages intended to make it easier for Go programs to work with SPDX files."
+third_party {
+ url {
+ type: GIT
+ value: "https://github.com/spdx/tools-golang.git"
+ }
+ version: "v0.4.0"
+ license_type: NOTICE
+ last_upgrade_date {
+ year: 2023
+ month: 1
+ day: 25
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ce1642a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,87 @@
+[![Build Status](https://github.com/spdx/tools-golang/workflows/build/badge.svg)](https://github.com/spdx/tools-golang/actions)
+[![Coverage Status](https://coveralls.io/repos/github/spdx/tools-golang/badge.svg)](https://coveralls.io/github/spdx/tools-golang)
+[![GitHub release](https://img.shields.io/github/release/spdx/tools-golang.svg)](https://github.com/spdx/tools-golang/releases/latest)
+[![Go Reference](https://pkg.go.dev/badge/github.com/spdx/tools-golang.svg)](https://pkg.go.dev/github.com/spdx/tools-golang)
+[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/5710/badge)](https://bestpractices.coreinfrastructure.org/projects/5710)
+
+# SPDX tools-golang
+
+tools-golang is a collection of Go packages intended to make it easier for
+Go programs to work with [SPDX®](https://spdx.dev/) files.
+
+## Recent news
+
+2022-01-11: **v0.4.0**: added support for SPDX v2.3 and YAML, as well as other
+improvements and bugfixes. See [RELEASE-NOTES.md](./RELEASE-NOTES.md) for full
+details.
+
+## What it does
+
+tools-golang currently works with files conformant to versions 2.1 and 2.2
+of the SPDX specification, available at: https://spdx.dev/specifications
+
+tools-golang provides the following packages:
+
+* *spdx* - in-memory data model for the sections of an SPDX document
+* *tvloader* - tag-value document loader
+* *tvsaver* - tag-value document saver
+* *rdfloader* - RDF document loader
+* *json* - JSON document parser and writer
+* *yaml* - YAML document parser and writer
+* *builder* - builds "empty" SPDX document (with hashes) for directory contents
+* *idsearcher* - searches for [SPDX short-form IDs](https://spdx.org/ids/) and builds SPDX document
+* *licensediff* - compares concluded licenses between files in two packages
+* *reporter* - generates basic license count report from SPDX document
+* *spdxlib* - various utility functions for manipulating SPDX documents in memory
+* *utils* - various utility functions that support the other tools-golang packages
+
+Examples for how to use these packages can be found in the `examples/`
+directory.
+
+## What it doesn't do
+
+tools-golang doesn't currently do any of the following:
+
+* work with files under any version of the SPDX spec prior to v2.1
+* convert between different versions of SPDX documents (e.g., from 2.1 to 2.2)
+* enable applications to interact with SPDX files without needing to care
+ (too much) about the particular SPDX file version
+
+We are working towards adding functionality for all of these. Code contributions
+are welcome!
+
+## Documentation
+
+SPDX tools-golang documentation is available on the pkg.go.dev website at https://pkg.go.dev/github.com/spdx/tools-golang.
+
+## Contributors
+
+Thank you to all of the contributors to spdx/tools-golang. A full list can be
+found in the GitHub repo and in [the release notes](RELEASE-NOTES.md).
+
+In particular, thank you to the following for major contributions:
+
+JSON parsing and saving support was added by @specter25 as part of his Google
+Summer of Code 2021 project.
+
+RDF parsing support was added by @RishabhBhatnagar as part of his Google Summer
+of Code 2020 project.
+
+## Licenses
+
+As indicated in `LICENSE-code`, tools-golang **source code files** are
+provided and may be used, at your option, under *either*:
+* Apache License, version 2.0 (**Apache-2.0**), **OR**
+* GNU General Public License, version 2.0 or later (**GPL-2.0-or-later**).
+
+As indicated in `LICENSE-docs`, tools-golang **documentation files** are
+provided and may be used under the Creative Commons Attribution
+4.0 International license (**CC-BY-4.0**).
+
+This `README.md` file is documentation:
+
+`SPDX-License-Identifier: CC-BY-4.0`
+
+## Security
+
+For security policy and reporting security issues, please refer to [SECURITY.md](SECURITY.md)
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
new file mode 100644
index 0000000..7ba18d9
--- /dev/null
+++ b/RELEASE-NOTES.md
@@ -0,0 +1,102 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+# Release Notes for spdx/tools-golang
+
+## 0.4.0
+
+0.4.0 released on 2022-01-11
+
+### New Features and Enhancements
+* SPDX v2.3 support #164
+* YAML support #134
+* Add reference types enumerables to SPDX pkg definition #162 #163
+* Expand hash algorithm support to include all valid SPDX 2.2 and 2.3 algorithms #173
+
+### Bug fixes
+* JSON encoding and decoding not properly handling SPDXRef- prefixes #170
+
+### Documentation and Cleanup
+* Overhaul structs, refactor JSON parser and saver #133
+* YAML documentation and JSON documentation fixes #141
+* Convert SPDX structs to versioned pkgs #146
+* Ensure consistency between JSON struct tags across different SPDX versions #174
+* Add Security.md for handling of security issues #154
+* Update build workflow to go 1.18 #148
+
+### Contributors
+* @ianling
+* @CatalinStratu
+* @lumjjb
+* @pxp928
+* @kzantow
+* @puerco
+* @jedevc
+
+## 0.3.0
+
+0.3.0 released on: 2022-04-03
+
+-rc1 released on: 2022-03-27
+
+### New Features and Enhancements
+* Add support for saving SPDX JSON: #92, #94, #97, #98, #104, #106, #113
+* Begin OpenSSF Best Practices process and add initial badge: #111
+ * also enabled branch protection for main branch
+
+### Bug fixes
+* tvsaver: Fix incorrect tag for Snippet IDs: #95
+* GitHub Actions: Fix incorrect branch for code coverage: #112
+* builder: Fix file paths to be relative rather than absolute: #114
+* builder: Add missing mandatory field LicenseInfoInFile: #119
+
+### Documentation and Cleanup
+* Fix link to release notes: #91
+* Language fixes for JSON documentation: #108
+* Add badges and links for releases and documentation: #109
+* Update documentation for release: #121, #122
+* Fixes for examples and sample run commands: #123, #125, #126, #127
+
+### Contributors
+* @CatalinStratu
+* @specter25
+* @swinslow
+
+## 0.2.0
+
+Released on: 2021-07-04
+
+### New Features and Enhancements
+* Add support for parsing SPDX JSON: #72, #75, #83, #84, #87
+ * bug fixes in interim versions: #77, #78, #79, #80, #81, #82
+* Improve handling of multiple hash checksum types: #41, #49, #60
+* Enable filtering relationships by various relationship types: #71, #74
+* Improve package license visibility: #65, #66
+* Rename primary branch to 'main': #69
+* Add release notes and push release: #85, #90
+
+### Bug fixes
+* Fix multiline (`<text>`) wrapping for various fields: #31, #53, #58, #89, #76
+* Fix special SPDX IDs in right-hand side of Relationships: #59, #63, #68
+* Throw error when parsing tag-value elements without SPDX IDs: #26, #64
+* Fix missing colon in 'excludes' for Package Verification Code when saving tag-value documents: #86, #88
+* Fix incorrect license statement: #70
+
+### Contributors
+* @autarch
+* @bisakhmondal
+* @ianling
+* @matthewkmayer
+* @RishabhBhatnagar
+* @specter25
+* @swinslow
+
+## 0.1.0
+
+Released on: 2021-03-20
+
+### Contributors
+* @abhishekspeer
+* @goneall
+* @RishabhBhatnagar
+* @rtgdk
+* @swinslow
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..c47b64c
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,3 @@
+# Security
+
+For security-related issues, do NOT open a public issue. Instead, please send an email to the list of [maintainers](/MAINTAINERS) with the subject header with prefix "spdx/golang-tools SECURITY:", so that we can respond to you in a timely manner.
diff --git a/builder/build.go b/builder/build.go
new file mode 100644
index 0000000..3c670a0
--- /dev/null
+++ b/builder/build.go
@@ -0,0 +1,224 @@
+// Package builder is used to create tools-golang data structures for a given
+// directory path's contents, with hashes, etc. filled in and with empty
+// license data.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package builder
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/builder/builder2v1"
+ "github.com/spdx/tools-golang/builder/builder2v2"
+ "github.com/spdx/tools-golang/builder/builder2v3"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 builder =====
+
+// Config2_1 is a collection of configuration settings for builder
+// (for version 2.1 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_1.
+type Config2_1 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // CreatorType should be one of "Person", "Organization" or "Tool".
+ // If not one of those strings, it will be interpreted as "Person".
+ CreatorType string
+
+ // Creator will be filled in for the given CreatorType.
+ Creator string
+
+ // PathsIgnored lists certain paths to be omitted from the built document.
+ // Each string should be a path, relative to the package's dirRoot,
+ // to a specific file or (for all files in a directory) ending in a slash.
+ // Prefix the string with "**" to omit all instances of that file /
+ // directory, regardless of where it is in the file tree.
+ PathsIgnored []string
+
+ // TestValues is used to pass fixed values for testing purposes
+ // only, and should be set to nil for production use. It is only
+ // exported so that it will be accessible within builder2v1.
+ TestValues map[string]string
+}
+
+// Build2_1 creates an SPDX Document (version 2.1), returning that document or
+// error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - config: Config object
+func Build2_1(packageName string, dirRoot string, config *Config2_1) (*v2_1.Document, error) {
+ // build Package section first -- will include Files and make the
+ // package verification code available
+ pkg, err := builder2v1.BuildPackageSection2_1(packageName, dirRoot, config.PathsIgnored)
+ if err != nil {
+ return nil, err
+ }
+
+ ci, err := builder2v1.BuildCreationInfoSection2_1(config.CreatorType, config.Creator, config.TestValues)
+ if err != nil {
+ return nil, err
+ }
+
+ rln, err := builder2v1.BuildRelationshipSection2_1(packageName)
+ if err != nil {
+ return nil, err
+ }
+
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: packageName,
+ DocumentNamespace: fmt.Sprintf("%s%s-%s", config.NamespacePrefix, packageName, pkg.PackageVerificationCode),
+ CreationInfo: ci,
+ Packages: []*v2_1.Package{pkg},
+ Relationships: []*v2_1.Relationship{rln},
+ }
+
+ return doc, nil
+}
+
+// ===== 2.2 builder =====
+
+// Config2_2 is a collection of configuration settings for builder
+// (for version 2.2 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_2.
+type Config2_2 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // CreatorType should be one of "Person", "Organization" or "Tool".
+ // If not one of those strings, it will be interpreted as "Person".
+ CreatorType string
+
+ // Creator will be filled in for the given CreatorType.
+ Creator string
+
+ // PathsIgnored lists certain paths to be omitted from the built document.
+ // Each string should be a path, relative to the package's dirRoot,
+ // to a specific file or (for all files in a directory) ending in a slash.
+ // Prefix the string with "**" to omit all instances of that file /
+ // directory, regardless of where it is in the file tree.
+ PathsIgnored []string
+
+ // TestValues is used to pass fixed values for testing purposes
+ // only, and should be set to nil for production use. It is only
+ // exported so that it will be accessible within builder2v2.
+ TestValues map[string]string
+}
+
+// Build2_2 creates an SPDX Document (version 2.2), returning that document or
+// error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - config: Config object
+func Build2_2(packageName string, dirRoot string, config *Config2_2) (*v2_2.Document, error) {
+ // build Package section first -- will include Files and make the
+ // package verification code available
+ pkg, err := builder2v2.BuildPackageSection2_2(packageName, dirRoot, config.PathsIgnored)
+ if err != nil {
+ return nil, err
+ }
+
+ ci, err := builder2v2.BuildCreationInfoSection2_2(config.CreatorType, config.Creator, config.TestValues)
+ if err != nil {
+ return nil, err
+ }
+
+ rln, err := builder2v2.BuildRelationshipSection2_2(packageName)
+ if err != nil {
+ return nil, err
+ }
+
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: packageName,
+ DocumentNamespace: fmt.Sprintf("%s%s-%s", config.NamespacePrefix, packageName, pkg.PackageVerificationCode),
+ CreationInfo: ci,
+ Packages: []*v2_2.Package{pkg},
+ Relationships: []*v2_2.Relationship{rln},
+ }
+
+ return doc, nil
+}
+
+// ===== 2.3 builder =====
+
+// Config2_3 is a collection of configuration settings for builder
+// (for version 2.3 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_3.
+type Config2_3 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // CreatorType should be one of "Person", "Organization" or "Tool".
+ // If not one of those strings, it will be interpreted as "Person".
+ CreatorType string
+
+ // Creator will be filled in for the given CreatorType.
+ Creator string
+
+ // PathsIgnored lists certain paths to be omitted from the built document.
+ // Each string should be a path, relative to the package's dirRoot,
+ // to a specific file or (for all files in a directory) ending in a slash.
+ // Prefix the string with "**" to omit all instances of that file /
+ // directory, regardless of where it is in the file tree.
+ PathsIgnored []string
+
+ // TestValues is used to pass fixed values for testing purposes
+ // only, and should be set to nil for production use. It is only
+ // exported so that it will be accessible within builder2v3.
+ TestValues map[string]string
+}
+
+// Build2_3 creates an SPDX Document (version 2.3), returning that document or
+// error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - config: Config object
+func Build2_3(packageName string, dirRoot string, config *Config2_3) (*v2_3.Document, error) {
+ // build Package section first -- will include Files and make the
+ // package verification code available
+ pkg, err := builder2v3.BuildPackageSection2_3(packageName, dirRoot, config.PathsIgnored)
+ if err != nil {
+ return nil, err
+ }
+
+ ci, err := builder2v3.BuildCreationInfoSection2_3(config.CreatorType, config.Creator, config.TestValues)
+ if err != nil {
+ return nil, err
+ }
+
+ rln, err := builder2v3.BuildRelationshipSection2_3(packageName)
+ if err != nil {
+ return nil, err
+ }
+
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: packageName,
+ DocumentNamespace: fmt.Sprintf("%s%s-%s", config.NamespacePrefix, packageName, pkg.PackageVerificationCode),
+ CreationInfo: ci,
+ Packages: []*v2_3.Package{pkg},
+ Relationships: []*v2_3.Relationship{rln},
+ }
+
+ return doc, nil
+}
diff --git a/builder/build_test.go b/builder/build_test.go
new file mode 100644
index 0000000..ff881de
--- /dev/null
+++ b/builder/build_test.go
@@ -0,0 +1,826 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== 2.1 Builder top-level Document test =====
+func TestBuild2_1CreatesDocument(t *testing.T) {
+ dirRoot := "../testdata/project1/"
+
+ config := &Config2_1{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ CreatorType: "Person",
+ Creator: "John Doe",
+ TestValues: make(map[string]string),
+ }
+ config.TestValues["Created"] = "2018-10-19T04:38:00Z"
+
+ wantVerificationCode := common.PackageVerificationCode{Value: "fc9ac4a370af0a471c2e52af66d6b4cf4e2ba12b"}
+
+ doc, err := Build2_1("project1", dirRoot, config)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // check CI section
+ if doc.CreationInfo == nil {
+ t.Fatalf("expected non-nil CreationInfo section, got nil")
+ }
+ if doc.SPDXVersion != "SPDX-2.1" {
+ t.Errorf("expected %s, got %s", "SPDX-2.1", doc.SPDXVersion)
+ }
+ if doc.DataLicense != "CC0-1.0" {
+ t.Errorf("expected %s, got %s", "CC0-1.0", doc.DataLicense)
+ }
+ if doc.SPDXIdentifier != common.ElementID("DOCUMENT") {
+ t.Errorf("expected %s, got %v", "DOCUMENT", doc.SPDXIdentifier)
+ }
+ if doc.DocumentName != "project1" {
+ t.Errorf("expected %s, got %s", "project1", doc.DocumentName)
+ }
+ wantNamespace := fmt.Sprintf("https://github.com/swinslow/spdx-docs/spdx-go/testdata-project1-%s", wantVerificationCode)
+ if doc.DocumentNamespace != wantNamespace {
+ t.Errorf("expected %s, got %s", wantNamespace, doc.DocumentNamespace)
+ }
+ if len(doc.CreationInfo.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(doc.CreationInfo.Creators))
+ }
+ if doc.CreationInfo.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %+v", "John Doe", doc.CreationInfo.Creators[1])
+ }
+ if doc.CreationInfo.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %+v", "github.com/spdx/tools-golang/builder", doc.CreationInfo.Creators[0])
+ }
+ if doc.CreationInfo.Created != "2018-10-19T04:38:00Z" {
+ t.Errorf("expected %s, got %s", "2018-10-19T04:38:00Z", doc.CreationInfo.Created)
+ }
+
+ // check Package section
+ if doc.Packages == nil {
+ t.Fatalf("expected non-nil doc.Packages, got nil")
+ }
+ if len(doc.Packages) != 1 {
+ t.Fatalf("expected %d, got %d", 1, len(doc.Packages))
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if pkg.PackageName != "project1" {
+ t.Errorf("expected %v, got %v", "project1", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != common.ElementID("Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", pkg.PackageSPDXIdentifier)
+ }
+ if pkg.PackageDownloadLocation != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageDownloadLocation)
+ }
+ if pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, pkg.FilesAnalyzed)
+ }
+ if pkg.PackageVerificationCode.Value != wantVerificationCode.Value {
+ t.Errorf("expected %v, got %v", wantVerificationCode, pkg.PackageVerificationCode)
+ }
+ if pkg.PackageLicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseConcluded)
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 0 {
+ t.Errorf("expected %v, got %v", 0, len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseDeclared != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageCopyrightText)
+ }
+
+ // check Files section
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+
+ // files should be in order of identifier, which is numeric,
+ // created based on alphabetical order of files:
+ // emptyfile, file1, file3, folder/file4, lastfile
+
+ // check emptyfile.testdata.txt
+ fileEmpty := pkg.Files[0]
+ if fileEmpty == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if fileEmpty.FileName != "./emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./emptyfile.testdata.txt", fileEmpty.FileName)
+ }
+ if fileEmpty.FileSPDXIdentifier != common.ElementID("File0") {
+ t.Errorf("expected %v, got %v", "File0", fileEmpty.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range fileEmpty.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
+ t.Errorf("expected %v, got %v", "da39a3ee5e6b4b0d3255bfef95601890afd80709", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
+ t.Errorf("expected %v, got %v", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "d41d8cd98f00b204e9800998ecf8427e" {
+ t.Errorf("expected %v, got %v", "d41d8cd98f00b204e9800998ecf8427e", checksum.Value)
+ }
+ }
+ }
+
+ if fileEmpty.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseConcluded)
+ }
+ if len(fileEmpty.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(fileEmpty.LicenseInfoInFiles))
+ } else {
+ if fileEmpty.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseInfoInFiles[0])
+ }
+ }
+ if fileEmpty.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.FileCopyrightText)
+ }
+
+ // check file1.testdata.txt
+ file1 := pkg.Files[1]
+ if file1 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file1.FileName != "./file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./file1.testdata.txt", file1.FileName)
+ }
+ if file1.FileSPDXIdentifier != common.ElementID("File1") {
+ t.Errorf("expected %v, got %v", "File1", file1.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file1.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", checksum.Value)
+ }
+ }
+ }
+ if file1.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseConcluded)
+ }
+ if len(file1.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file1.LicenseInfoInFiles))
+ } else {
+ if file1.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseInfoInFiles[0])
+ }
+ }
+ if file1.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.FileCopyrightText)
+ }
+
+ // check file3.testdata.txt
+ file3 := pkg.Files[2]
+ if file3 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file3.FileName != "./file3.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./file3.testdata.txt", file3.FileName)
+ }
+ if file3.FileSPDXIdentifier != common.ElementID("File2") {
+ t.Errorf("expected %v, got %v", "File2", file3.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file3.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "a46114b70e163614f01c64adf44cdd438f158fce" {
+ t.Errorf("expected %v, got %v", "a46114b70e163614f01c64adf44cdd438f158fce", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "9fc181b9892720a15df1a1e561860318db40621bd4040ccdf18e110eb01d04b4" {
+ t.Errorf("expected %v, got %v", "9fc181b9892720a15df1a1e561860318db40621bd4040ccdf18e110eb01d04b4", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "3e02d3ab9c58eec6911dbba37570934f" {
+ t.Errorf("expected %v, got %v", "3e02d3ab9c58eec6911dbba37570934f", checksum.Value)
+ }
+ }
+ }
+ if file3.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file3.LicenseConcluded)
+ }
+ if len(file3.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file3.LicenseInfoInFiles))
+ } else {
+ if file3.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file3.LicenseInfoInFiles[0])
+ }
+ }
+ if file3.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file3.FileCopyrightText)
+ }
+
+ // check folder1/file4.testdata.txt
+ file4 := pkg.Files[3]
+ if file4 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file4.FileName != "./folder1/file4.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./folder1/file4.testdata.txt", file4.FileName)
+ }
+ if file4.FileSPDXIdentifier != common.ElementID("File3") {
+ t.Errorf("expected %v, got %v", "File3", file4.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file4.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "e623d7d7d782a7c8323c4d436acee4afab34320f" {
+ t.Errorf("expected %v, got %v", "e623d7d7d782a7c8323c4d436acee4afab34320f", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "574fa42c5e0806c0f8906a44884166540206f021527729407cd5326838629c59" {
+ t.Errorf("expected %v, got %v", "574fa42c5e0806c0f8906a44884166540206f021527729407cd5326838629c59", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "96e6a25d35df5b1c477710ef4d0c7210" {
+ t.Errorf("expected %v, got %v", "96e6a25d35df5b1c477710ef4d0c7210", checksum.Value)
+ }
+ }
+ }
+ if file4.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file4.LicenseConcluded)
+ }
+ if len(file4.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file4.LicenseInfoInFiles))
+ } else {
+ if file4.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file4.LicenseInfoInFiles[0])
+ }
+ }
+ if file4.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file4.FileCopyrightText)
+ }
+
+ // check lastfile.testdata.txt
+ lastfile := pkg.Files[4]
+ if lastfile == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if lastfile.FileName != "./lastfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./lastfile.testdata.txt", lastfile.FileName)
+ }
+ if lastfile.FileSPDXIdentifier != common.ElementID("File4") {
+ t.Errorf("expected %v, got %v", "File4", lastfile.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range lastfile.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "26d6221d682d9ba59116f9753a701f34271c8ce1" {
+ t.Errorf("expected %v, got %v", "26d6221d682d9ba59116f9753a701f34271c8ce1", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "0a4bdaf990e9b330ff72022dd78110ae98b60e08337cf2105b89856373416805" {
+ t.Errorf("expected %v, got %v", "0a4bdaf990e9b330ff72022dd78110ae98b60e08337cf2105b89856373416805", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "f60baa793870d9085461ad6bbab50b7f" {
+ t.Errorf("expected %v, got %v", "f60baa793870d9085461ad6bbab50b7f", checksum.Value)
+ }
+ }
+ }
+ if lastfile.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", lastfile.LicenseConcluded)
+ }
+ if len(lastfile.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(lastfile.LicenseInfoInFiles))
+ } else {
+ if lastfile.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", lastfile.LicenseInfoInFiles[0])
+ }
+ }
+ if lastfile.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", lastfile.FileCopyrightText)
+ }
+
+ // check Relationship section -- should be a relationship for doc DESCRIBES pkg
+ if doc.Relationships == nil {
+ t.Fatalf("expected non-nil Relationships section, got nil")
+ }
+ if len(doc.Relationships) == 0 {
+ t.Fatalf("expected %v, got %v", 0, len(doc.Relationships))
+ }
+ rln := doc.Relationships[0]
+ if rln == nil {
+ t.Fatalf("expected non-nil Relationship, got nil")
+ }
+ if rln.RefA != common.MakeDocElementID("", "DOCUMENT") {
+ t.Errorf("expected %v, got %v", "DOCUMENT", rln.RefA)
+ }
+ if rln.RefB != common.MakeDocElementID("", "Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", rln.RefB)
+ }
+ if rln.Relationship != "DESCRIBES" {
+ t.Errorf("expected %v, got %v", "DESCRIBES", rln.Relationship)
+ }
+
+ // and check that other sections are present, but empty
+ if doc.OtherLicenses != nil {
+ t.Fatalf("expected nil OtherLicenses section, got non-nil")
+ }
+ if doc.Annotations != nil {
+ t.Fatalf("expected nil Annotations section, got non-nil")
+ }
+ if doc.Reviews != nil {
+ t.Fatalf("expected nil Reviews section, got non-nil")
+ }
+
+}
+
+func TestBuild2_1CanIgnoreFiles(t *testing.T) {
+ dirRoot := "../testdata/project3/"
+
+ config := &Config2_1{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ CreatorType: "Person",
+ Creator: "John Doe",
+ PathsIgnored: []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ },
+ TestValues: make(map[string]string),
+ }
+ config.TestValues["Created"] = "2018-10-19T04:38:00Z"
+
+ doc, err := Build2_1("project1", dirRoot, config)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected len %d, got %d", 5, len(pkg.Files))
+ }
+
+ want := "./dontscan.txt"
+ got := pkg.Files[0].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep/keep.txt"
+ got = pkg.Files[1].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep.txt"
+ got = pkg.Files[2].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/dontscan.txt"
+ got = pkg.Files[3].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/keep.txt"
+ got = pkg.Files[4].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
+
+// ===== 2.2 Builder top-level Document test =====
+func TestBuild2_2CreatesDocument(t *testing.T) {
+ dirRoot := "../testdata/project1/"
+
+ config := &Config2_2{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ CreatorType: "Person",
+ Creator: "John Doe",
+ TestValues: make(map[string]string),
+ }
+ config.TestValues["Created"] = "2018-10-19T04:38:00Z"
+
+ wantVerificationCode := common.PackageVerificationCode{Value: "fc9ac4a370af0a471c2e52af66d6b4cf4e2ba12b"}
+
+ doc, err := Build2_2("project1", dirRoot, config)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // check CI section
+ if doc.CreationInfo == nil {
+ t.Fatalf("expected non-nil CreationInfo section, got nil")
+ }
+ if doc.SPDXVersion != "SPDX-2.2" {
+ t.Errorf("expected %s, got %s", "SPDX-2.2", doc.SPDXVersion)
+ }
+ if doc.DataLicense != "CC0-1.0" {
+ t.Errorf("expected %s, got %s", "CC0-1.0", doc.DataLicense)
+ }
+ if doc.SPDXIdentifier != common.ElementID("DOCUMENT") {
+ t.Errorf("expected %s, got %v", "DOCUMENT", doc.SPDXIdentifier)
+ }
+ if doc.DocumentName != "project1" {
+ t.Errorf("expected %s, got %s", "project1", doc.DocumentName)
+ }
+ wantNamespace := fmt.Sprintf("https://github.com/swinslow/spdx-docs/spdx-go/testdata-project1-%s", wantVerificationCode)
+ if doc.DocumentNamespace != wantNamespace {
+ t.Errorf("expected %s, got %s", wantNamespace, doc.DocumentNamespace)
+ }
+ if len(doc.CreationInfo.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(doc.CreationInfo.Creators))
+ }
+ if doc.CreationInfo.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %+v", "John Doe", doc.CreationInfo.Creators[1])
+ }
+ if doc.CreationInfo.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %+v", "github.com/spdx/tools-golang/builder", doc.CreationInfo.Creators[0])
+ }
+ if doc.CreationInfo.Created != "2018-10-19T04:38:00Z" {
+ t.Errorf("expected %s, got %s", "2018-10-19T04:38:00Z", doc.CreationInfo.Created)
+ }
+
+ // check Package section
+ if doc.Packages == nil {
+ t.Fatalf("expected non-nil doc.Packages, got nil")
+ }
+ if len(doc.Packages) != 1 {
+ t.Fatalf("expected %d, got %d", 1, len(doc.Packages))
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if pkg.PackageName != "project1" {
+ t.Errorf("expected %v, got %v", "project1", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != common.ElementID("Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", pkg.PackageSPDXIdentifier)
+ }
+ if pkg.PackageDownloadLocation != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageDownloadLocation)
+ }
+ if pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, pkg.FilesAnalyzed)
+ }
+ if pkg.PackageVerificationCode.Value != wantVerificationCode.Value {
+ t.Errorf("expected %v, got %v", wantVerificationCode, pkg.PackageVerificationCode)
+ }
+ if pkg.PackageLicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseConcluded)
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 0 {
+ t.Errorf("expected %v, got %v", 0, len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseDeclared != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageCopyrightText)
+ }
+
+ // check Files section
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+
+ // files should be in order of identifier, which is numeric,
+ // created based on alphabetical order of files:
+ // emptyfile, file1, file3, folder/file4, lastfile
+
+ // check emptyfile.testdata.txt
+ fileEmpty := pkg.Files[0]
+ if fileEmpty == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if fileEmpty.FileName != "./emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./emptyfile.testdata.txt", fileEmpty.FileName)
+ }
+ if fileEmpty.FileSPDXIdentifier != common.ElementID("File0") {
+ t.Errorf("expected %v, got %v", "File0", fileEmpty.FileSPDXIdentifier)
+ }
+ for _, checksum := range fileEmpty.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
+ t.Errorf("expected %v, got %v", "da39a3ee5e6b4b0d3255bfef95601890afd80709", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
+ t.Errorf("expected %v, got %v", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "d41d8cd98f00b204e9800998ecf8427e" {
+ t.Errorf("expected %v, got %v", "d41d8cd98f00b204e9800998ecf8427e", checksum.Value)
+ }
+ }
+ }
+ if fileEmpty.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseConcluded)
+ }
+ if len(fileEmpty.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(fileEmpty.LicenseInfoInFiles))
+ } else {
+ if fileEmpty.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseInfoInFiles[0])
+ }
+ }
+ if fileEmpty.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.FileCopyrightText)
+ }
+
+ // check file1.testdata.txt
+ file1 := pkg.Files[1]
+ if file1 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file1.FileName != "./file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./file1.testdata.txt", file1.FileName)
+ }
+ if file1.FileSPDXIdentifier != common.ElementID("File1") {
+ t.Errorf("expected %v, got %v", "File1", file1.FileSPDXIdentifier)
+ }
+ for _, checksum := range file1.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", checksum.Value)
+ }
+ }
+ }
+ if file1.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseConcluded)
+ }
+ if len(file1.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file1.LicenseInfoInFiles))
+ } else {
+ if file1.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseInfoInFiles[0])
+ }
+ }
+ if file1.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.FileCopyrightText)
+ }
+
+ // check file3.testdata.txt
+ file3 := pkg.Files[2]
+ if file3 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file3.FileName != "./file3.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./file3.testdata.txt", file3.FileName)
+ }
+ if file3.FileSPDXIdentifier != common.ElementID("File2") {
+ t.Errorf("expected %v, got %v", "File2", file3.FileSPDXIdentifier)
+ }
+ for _, checksum := range file3.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "a46114b70e163614f01c64adf44cdd438f158fce" {
+ t.Errorf("expected %v, got %v", "a46114b70e163614f01c64adf44cdd438f158fce", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "9fc181b9892720a15df1a1e561860318db40621bd4040ccdf18e110eb01d04b4" {
+ t.Errorf("expected %v, got %v", "9fc181b9892720a15df1a1e561860318db40621bd4040ccdf18e110eb01d04b4", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "3e02d3ab9c58eec6911dbba37570934f" {
+ t.Errorf("expected %v, got %v", "3e02d3ab9c58eec6911dbba37570934f", checksum.Value)
+ }
+ }
+ }
+ if file3.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file3.LicenseConcluded)
+ }
+ if len(file3.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file3.LicenseInfoInFiles))
+ } else {
+ if file3.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file3.LicenseInfoInFiles[0])
+ }
+ }
+ if file3.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file3.FileCopyrightText)
+ }
+
+ // check folder1/file4.testdata.txt
+ file4 := pkg.Files[3]
+ if file4 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file4.FileName != "./folder1/file4.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./folder1/file4.testdata.txt", file4.FileName)
+ }
+ if file4.FileSPDXIdentifier != common.ElementID("File3") {
+ t.Errorf("expected %v, got %v", "File3", file4.FileSPDXIdentifier)
+ }
+ for _, checksum := range file4.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "e623d7d7d782a7c8323c4d436acee4afab34320f" {
+ t.Errorf("expected %v, got %v", "e623d7d7d782a7c8323c4d436acee4afab34320f", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "574fa42c5e0806c0f8906a44884166540206f021527729407cd5326838629c59" {
+ t.Errorf("expected %v, got %v", "574fa42c5e0806c0f8906a44884166540206f021527729407cd5326838629c59", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "96e6a25d35df5b1c477710ef4d0c7210" {
+ t.Errorf("expected %v, got %v", "96e6a25d35df5b1c477710ef4d0c7210", checksum.Value)
+ }
+ }
+ }
+ if file4.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file4.LicenseConcluded)
+ }
+ if len(file4.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file4.LicenseInfoInFiles))
+ } else {
+ if file4.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file4.LicenseInfoInFiles[0])
+ }
+ }
+ if file4.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file4.FileCopyrightText)
+ }
+
+ // check lastfile.testdata.txt
+ lastfile := pkg.Files[4]
+ if lastfile == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if lastfile.FileName != "./lastfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/lastfile.testdata.txt", lastfile.FileName)
+ }
+ if lastfile.FileSPDXIdentifier != common.ElementID("File4") {
+ t.Errorf("expected %v, got %v", "File4", lastfile.FileSPDXIdentifier)
+ }
+ for _, checksum := range lastfile.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "26d6221d682d9ba59116f9753a701f34271c8ce1" {
+ t.Errorf("expected %v, got %v", "26d6221d682d9ba59116f9753a701f34271c8ce1", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "0a4bdaf990e9b330ff72022dd78110ae98b60e08337cf2105b89856373416805" {
+ t.Errorf("expected %v, got %v", "0a4bdaf990e9b330ff72022dd78110ae98b60e08337cf2105b89856373416805", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "f60baa793870d9085461ad6bbab50b7f" {
+ t.Errorf("expected %v, got %v", "f60baa793870d9085461ad6bbab50b7f", checksum.Value)
+ }
+ }
+ }
+ if lastfile.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", lastfile.LicenseConcluded)
+ }
+ if len(lastfile.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(lastfile.LicenseInfoInFiles))
+ } else {
+ if lastfile.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", lastfile.LicenseInfoInFiles[0])
+ }
+ }
+ if lastfile.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", lastfile.FileCopyrightText)
+ }
+
+ // check Relationship section -- should be a relationship for doc DESCRIBES pkg
+ if doc.Relationships == nil {
+ t.Fatalf("expected non-nil Relationships section, got nil")
+ }
+ if len(doc.Relationships) == 0 {
+ t.Fatalf("expected %v, got %v", 0, len(doc.Relationships))
+ }
+ rln := doc.Relationships[0]
+ if rln == nil {
+ t.Fatalf("expected non-nil Relationship, got nil")
+ }
+ if rln.RefA != common.MakeDocElementID("", "DOCUMENT") {
+ t.Errorf("expected %v, got %v", "DOCUMENT", rln.RefA)
+ }
+ if rln.RefB != common.MakeDocElementID("", "Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", rln.RefB)
+ }
+ if rln.Relationship != "DESCRIBES" {
+ t.Errorf("expected %v, got %v", "DESCRIBES", rln.Relationship)
+ }
+
+ // and check that other sections are present, but empty
+ if doc.OtherLicenses != nil {
+ t.Fatalf("expected nil OtherLicenses section, got non-nil")
+ }
+ if doc.Annotations != nil {
+ t.Fatalf("expected nil Annotations section, got non-nil")
+ }
+ if doc.Reviews != nil {
+ t.Fatalf("expected nil Reviews section, got non-nil")
+ }
+
+}
+
+func TestBuild2_2CanIgnoreFiles(t *testing.T) {
+ dirRoot := "../testdata/project3/"
+
+ config := &Config2_2{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ CreatorType: "Person",
+ Creator: "John Doe",
+ PathsIgnored: []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ },
+ TestValues: make(map[string]string),
+ }
+ config.TestValues["Created"] = "2018-10-19T04:38:00Z"
+
+ doc, err := Build2_2("project1", dirRoot, config)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected len %d, got %d", 5, len(pkg.Files))
+ }
+
+ want := "./dontscan.txt"
+ got := pkg.Files[0].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep/keep.txt"
+ got = pkg.Files[1].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep.txt"
+ got = pkg.Files[2].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/dontscan.txt"
+ got = pkg.Files[3].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/keep.txt"
+ got = pkg.Files[4].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
diff --git a/builder/builder2v1/build_creation_info.go b/builder/builder2v1/build_creation_info.go
new file mode 100644
index 0000000..e128fb9
--- /dev/null
+++ b/builder/builder2v1/build_creation_info.go
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "time"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// BuildCreationInfoSection2_1 creates an SPDX Package (version 2.1), returning that
+// package or error if any is encountered. Arguments:
+// - creatorType: one of Person, Organization or Tool
+// - creator: creator string
+// - testValues: for testing only; call with nil when using in production
+func BuildCreationInfoSection2_1(creatorType string, creator string, testValues map[string]string) (*v2_1.CreationInfo, error) {
+ // build creator slices
+ creators := []common.Creator{
+ // add builder as a tool
+ {
+ Creator: "github.com/spdx/tools-golang/builder",
+ CreatorType: "Tool",
+ },
+ {
+ Creator: creator,
+ CreatorType: creatorType,
+ },
+ }
+
+ // use test Created time if passing test values
+ location, _ := time.LoadLocation("UTC")
+ locationTime := time.Now().In(location)
+ created := locationTime.Format("2006-01-02T15:04:05Z")
+ if testVal := testValues["Created"]; testVal != "" {
+ created = testVal
+ }
+
+ ci := &v2_1.CreationInfo{
+ Creators: creators,
+ Created: created,
+ }
+ return ci, nil
+}
diff --git a/builder/builder2v1/build_creation_info_test.go b/builder/builder2v1/build_creation_info_test.go
new file mode 100644
index 0000000..9684fde
--- /dev/null
+++ b/builder/builder2v1/build_creation_info_test.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "testing"
+)
+
+// ===== CreationInfo section builder tests =====
+func TestBuilder2_1CanBuildCreationInfoSection(t *testing.T) {
+ creatorType := "Organization"
+ creator := "Jane Doe LLC"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_1(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "Jane Doe LLC" {
+ t.Errorf("expected %s, got %s", "Jane Doe LLC", ci.Creators[1].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0].Creator)
+ }
+ if ci.Created != "2018-10-20T16:48:00Z" {
+ t.Errorf("expected %s, got %s", "2018-10-20T16:48:00Z", ci.Created)
+ }
+}
+
+func TestBuilder2_1CanBuildCreationInfoSectionWithCreatorPerson(t *testing.T) {
+ creatorType := "Person"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_1(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[1].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0].Creator)
+ }
+}
+
+func TestBuilder2_1CanBuildCreationInfoSectionWithCreatorTool(t *testing.T) {
+ creatorType := "Tool"
+ creator := "some-other-tool-2.1"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_1(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+ if ci.Creators[1].Creator != "some-other-tool-2.1" {
+ t.Errorf("expected %s, got %s", "some-other-tool-2.1", ci.Creators[1])
+ }
+}
+
+func TestBuilder2_1CanBuildCreationInfoSectionWithInvalidPerson(t *testing.T) {
+ creatorType := "Whatever"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_1(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[1])
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+}
diff --git a/builder/builder2v1/build_file.go b/builder/builder2v1/build_file.go
new file mode 100644
index 0000000..57cc2bc
--- /dev/null
+++ b/builder/builder2v1/build_file.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildFileSection2_1 creates an SPDX File (version 2.1), returning that
+// file or error if any is encountered. Arguments:
+// - filePath: path to file, relative to prefix
+// - prefix: relative directory for filePath
+// - fileNumber: integer index (unique within package) to use in identifier
+func BuildFileSection2_1(filePath string, prefix string, fileNumber int) (*v2_1.File, error) {
+ // build the full file path
+ p := filepath.Join(prefix, filePath)
+
+ // make sure we can get the file and its hashes
+ ssha1, ssha256, smd5, err := utils.GetHashesForFilePath(p)
+ if err != nil {
+ return nil, err
+ }
+
+ // build the identifier
+ i := fmt.Sprintf("File%d", fileNumber)
+
+ // now build the File section
+ f := &v2_1.File{
+ FileName: filePath,
+ FileSPDXIdentifier: common.ElementID(i),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: ssha1,
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: ssha256,
+ },
+ {
+ Algorithm: common.MD5,
+ Value: smd5,
+ },
+ },
+ LicenseConcluded: "NOASSERTION",
+ LicenseInfoInFiles: []string{"NOASSERTION"},
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ return f, nil
+}
diff --git a/builder/builder2v1/build_file_test.go b/builder/builder2v1/build_file_test.go
new file mode 100644
index 0000000..5da0390
--- /dev/null
+++ b/builder/builder2v1/build_file_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== File section builder tests =====
+func TestBuilder2_1CanBuildFileSection(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "../../testdata/project1/"
+ fileNumber := 17
+
+ file1, err := BuildFileSection2_1(filePath, prefix, fileNumber)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if file1 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file1.FileName != "/file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/file1.testdata.txt", file1.FileName)
+ }
+ if file1.FileSPDXIdentifier != common.ElementID("File17") {
+ t.Errorf("expected %v, got %v", "File17", file1.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file1.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", checksum.Value)
+ }
+ }
+ }
+
+ if file1.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseConcluded)
+ }
+ if len(file1.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file1.LicenseInfoInFiles))
+ } else {
+ if file1.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseInfoInFiles[0])
+ }
+ }
+ if file1.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.FileCopyrightText)
+ }
+
+}
+
+func TestBuilder2_1BuildFileSectionFailsForInvalidFilePath(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "oops/wrong/path"
+ fileNumber := 11
+
+ _, err := BuildFileSection2_1(filePath, prefix, fileNumber)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/builder/builder2v1/build_package.go b/builder/builder2v1/build_package.go
new file mode 100644
index 0000000..7d8ddbb
--- /dev/null
+++ b/builder/builder2v1/build_package.go
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "runtime"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildPackageSection2_1 creates an SPDX Package (version 2.1), returning
+// that package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - pathsIgnore: slice of strings for filepaths to ignore
+func BuildPackageSection2_1(packageName string, dirRoot string, pathsIgnore []string) (*v2_1.Package, error) {
+ // build the file section first, so we'll have it available
+ // for calculating the package verification code
+ filepaths, err := utils.GetAllFilePaths(dirRoot, pathsIgnore)
+ if err != nil {
+ return nil, err
+ }
+ osType := runtime.GOOS
+
+ re, ok := regexp.Compile("/+")
+ if ok != nil {
+ return nil, err
+ }
+
+ dirRootLen := 0
+ if osType == "windows" {
+ dirRootLen = len(dirRoot)
+ }
+
+ files := []*v2_1.File{}
+ fileNumber := 0
+ for _, fp := range filepaths {
+ newFilePatch := ""
+ if osType == "windows" {
+ newFilePatch = filepath.FromSlash("." + fp[dirRootLen:])
+ } else {
+ newFilePatch = filepath.FromSlash("./" + fp)
+ }
+ newFile, err := BuildFileSection2_1(re.ReplaceAllLiteralString(newFilePatch, string(filepath.Separator)), dirRoot, fileNumber)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, newFile)
+ fileNumber++
+ }
+ // get the verification code
+ code, err := utils.GetVerificationCode2_1(files, "")
+ if err != nil {
+ return nil, err
+ }
+
+ // now build the package section
+ pkg := &v2_1.Package{
+ PackageName: packageName,
+ PackageSPDXIdentifier: common.ElementID(fmt.Sprintf("Package-%s", packageName)),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: code,
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{},
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: files,
+ }
+
+ return pkg, nil
+}
diff --git a/builder/builder2v1/build_package_test.go b/builder/builder2v1/build_package_test.go
new file mode 100644
index 0000000..959ae65
--- /dev/null
+++ b/builder/builder2v1/build_package_test.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Package section builder tests =====
+func TestBuilder2_1CanBuildPackageSection(t *testing.T) {
+ packageName := "project1"
+ dirRoot := "../../testdata/project1/"
+
+ wantVerificationCode := common.PackageVerificationCode{Value: "fc9ac4a370af0a471c2e52af66d6b4cf4e2ba12b"}
+
+ pkg, err := BuildPackageSection2_1(packageName, dirRoot, nil)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if pkg == nil {
+ t.Fatalf("expected non-nil Package, got nil")
+ }
+ if pkg.PackageName != "project1" {
+ t.Errorf("expected %v, got %v", "project1", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != common.ElementID("Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", pkg.PackageSPDXIdentifier)
+ }
+ if pkg.PackageDownloadLocation != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageDownloadLocation)
+ }
+ if pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, pkg.FilesAnalyzed)
+ }
+ if pkg.IsFilesAnalyzedTagPresent != true {
+ t.Errorf("expected %v, got %v", true, pkg.IsFilesAnalyzedTagPresent)
+ }
+ if pkg.PackageVerificationCode.Value != wantVerificationCode.Value {
+ t.Errorf("expected %v, got %v", wantVerificationCode, pkg.PackageVerificationCode)
+ }
+ if pkg.PackageLicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseConcluded)
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 0 {
+ t.Errorf("expected %v, got %v", 0, len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseDeclared != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageCopyrightText)
+ }
+
+ // and make sure we got the right number of files, and check the first one
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+ fileEmpty := pkg.Files[0]
+ if fileEmpty == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if fileEmpty.FileName != "./emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./emptyfile.testdata.txt", fileEmpty.FileName)
+ }
+ if fileEmpty.FileSPDXIdentifier != common.ElementID("File0") {
+ t.Errorf("expected %v, got %v", "File0", fileEmpty.FileSPDXIdentifier)
+ }
+ for _, checksum := range fileEmpty.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
+ t.Errorf("expected %v, got %v", "da39a3ee5e6b4b0d3255bfef95601890afd80709", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
+ t.Errorf("expected %v, got %v", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "d41d8cd98f00b204e9800998ecf8427e" {
+ t.Errorf("expected %v, got %v", "d41d8cd98f00b204e9800998ecf8427e", checksum.Value)
+ }
+ }
+ }
+ if fileEmpty.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseConcluded)
+ }
+ if len(fileEmpty.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(fileEmpty.LicenseInfoInFiles))
+ } else {
+ if fileEmpty.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseInfoInFiles[0])
+ }
+ }
+ if fileEmpty.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.FileCopyrightText)
+ }
+}
+
+func TestBuilder2_1CanIgnoreFiles(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../../testdata/project3/"
+ pathsIgnored := []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ }
+ pkg, err := BuildPackageSection2_1(packageName, dirRoot, pathsIgnored)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ // make sure we got the right files
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+
+ want := "./dontscan.txt"
+ got := pkg.Files[0].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep/keep.txt"
+ got = pkg.Files[1].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep.txt"
+ got = pkg.Files[2].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/dontscan.txt"
+ got = pkg.Files[3].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/keep.txt"
+ got = pkg.Files[4].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
diff --git a/builder/builder2v1/build_relationship.go b/builder/builder2v1/build_relationship.go
new file mode 100644
index 0000000..9f11d58
--- /dev/null
+++ b/builder/builder2v1/build_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// BuildRelationshipSection2_1 creates an SPDX Relationship (version 2.1)
+// solely for the document "DESCRIBES" package relationship, returning that
+// relationship or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+func BuildRelationshipSection2_1(packageName string) (*v2_1.Relationship, error) {
+ rln := &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", fmt.Sprintf("Package-%s", packageName)),
+ Relationship: "DESCRIBES",
+ }
+
+ return rln, nil
+}
diff --git a/builder/builder2v1/build_relationship_test.go b/builder/builder2v1/build_relationship_test.go
new file mode 100644
index 0000000..a24dedc
--- /dev/null
+++ b/builder/builder2v1/build_relationship_test.go
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Relationship section builder tests =====
+func TestBuilder2_1CanBuildRelationshipSection(t *testing.T) {
+ packageName := "project17"
+
+ rln, err := BuildRelationshipSection2_1(packageName)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if rln == nil {
+ t.Fatalf("expected non-nil relationship, got nil")
+ }
+ if rln.RefA != common.MakeDocElementID("", "DOCUMENT") {
+ t.Errorf("expected %v, got %v", "DOCUMENT", rln.RefA)
+ }
+ if rln.RefB != common.MakeDocElementID("", "Package-project17") {
+ t.Errorf("expected %v, got %v", "Package-project17", rln.RefB)
+ }
+ if rln.Relationship != "DESCRIBES" {
+ t.Errorf("expected %v, got %v", "DESCRIBES", rln.Relationship)
+ }
+
+}
diff --git a/builder/builder2v2/build_creation_info.go b/builder/builder2v2/build_creation_info.go
new file mode 100644
index 0000000..74b43fb
--- /dev/null
+++ b/builder/builder2v2/build_creation_info.go
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "time"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// BuildCreationInfoSection2_2 creates an SPDX Package (version 2.2), returning that
+// package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - code: verification code from Package
+// - namespacePrefix: prefix for DocumentNamespace (packageName and code will be added)
+// - creatorType: one of Person, Organization or Tool
+// - creator: creator string
+// - testValues: for testing only; call with nil when using in production
+func BuildCreationInfoSection2_2(creatorType string, creator string, testValues map[string]string) (*v2_2.CreationInfo, error) {
+ // build creator slices
+ creators := []common.Creator{
+ // add builder as a tool
+ {
+ Creator: "github.com/spdx/tools-golang/builder",
+ CreatorType: "Tool",
+ },
+ {
+ Creator: creator,
+ CreatorType: creatorType,
+ },
+ }
+
+ // use test Created time if passing test values
+ location, _ := time.LoadLocation("UTC")
+ locationTime := time.Now().In(location)
+ created := locationTime.Format("2006-01-02T15:04:05Z")
+ if testVal := testValues["Created"]; testVal != "" {
+ created = testVal
+ }
+
+ ci := &v2_2.CreationInfo{
+ Creators: creators,
+ Created: created,
+ }
+ return ci, nil
+}
diff --git a/builder/builder2v2/build_creation_info_test.go b/builder/builder2v2/build_creation_info_test.go
new file mode 100644
index 0000000..48a0654
--- /dev/null
+++ b/builder/builder2v2/build_creation_info_test.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "testing"
+)
+
+// ===== CreationInfo section builder tests =====
+func TestBuilder2_2CanBuildCreationInfoSection(t *testing.T) {
+ creatorType := "Organization"
+ creator := "Jane Doe LLC"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_2(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "Jane Doe LLC" {
+ t.Errorf("expected %s, got %s", "Jane Doe LLC", ci.Creators[0].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[1].Creator)
+ }
+ if ci.Created != "2018-10-20T16:48:00Z" {
+ t.Errorf("expected %s, got %s", "2018-10-20T16:48:00Z", ci.Created)
+ }
+}
+
+func TestBuilder2_2CanBuildCreationInfoSectionWithCreatorPerson(t *testing.T) {
+ creatorType := "Person"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_2(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[0].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[1].Creator)
+ }
+}
+
+func TestBuilder2_2CanBuildCreationInfoSectionWithCreatorTool(t *testing.T) {
+ creatorType := "Tool"
+ creator := "some-other-tool-2.1"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_2(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+ if ci.Creators[1].Creator != "some-other-tool-2.1" {
+ t.Errorf("expected %s, got %s", "some-other-tool-2.1", ci.Creators[1])
+ }
+}
+
+func TestBuilder2_2CanBuildCreationInfoSectionWithInvalidPerson(t *testing.T) {
+ creatorType := "Whatever"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_2(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[1])
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+}
diff --git a/builder/builder2v2/build_file.go b/builder/builder2v2/build_file.go
new file mode 100644
index 0000000..4e69f58
--- /dev/null
+++ b/builder/builder2v2/build_file.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildFileSection2_2 creates an SPDX File (version 2.2), returning that
+// file or error if any is encountered. Arguments:
+// - filePath: path to file, relative to prefix
+// - prefix: relative directory for filePath
+// - fileNumber: integer index (unique within package) to use in identifier
+func BuildFileSection2_2(filePath string, prefix string, fileNumber int) (*v2_2.File, error) {
+ // build the full file path
+ p := filepath.Join(prefix, filePath)
+
+ // make sure we can get the file and its hashes
+ ssha1, ssha256, smd5, err := utils.GetHashesForFilePath(p)
+ if err != nil {
+ return nil, err
+ }
+
+ // build the identifier
+ i := fmt.Sprintf("File%d", fileNumber)
+
+ // now build the File section
+ f := &v2_2.File{
+ FileName: filePath,
+ FileSPDXIdentifier: common.ElementID(i),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: ssha1,
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: ssha256,
+ },
+ {
+ Algorithm: common.MD5,
+ Value: smd5,
+ },
+ },
+ LicenseConcluded: "NOASSERTION",
+ LicenseInfoInFiles: []string{"NOASSERTION"},
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ return f, nil
+}
diff --git a/builder/builder2v2/build_file_test.go b/builder/builder2v2/build_file_test.go
new file mode 100644
index 0000000..8691583
--- /dev/null
+++ b/builder/builder2v2/build_file_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== File section builder tests =====
+func TestBuilder2_2CanBuildFileSection(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "../../testdata/project1/"
+ fileNumber := 17
+
+ file1, err := BuildFileSection2_2(filePath, prefix, fileNumber)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if file1 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file1.FileName != "/file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/file1.testdata.txt", file1.FileName)
+ }
+ if file1.FileSPDXIdentifier != common.ElementID("File17") {
+ t.Errorf("expected %v, got %v", "File17", file1.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file1.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", checksum.Value)
+ }
+ }
+ }
+
+ if file1.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseConcluded)
+ }
+ if len(file1.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file1.LicenseInfoInFiles))
+ } else {
+ if file1.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseInfoInFiles[0])
+ }
+ }
+ if file1.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.FileCopyrightText)
+ }
+
+}
+
+func TestBuilder2_2BuildFileSectionFailsForInvalidFilePath(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "oops/wrong/path"
+ fileNumber := 11
+
+ _, err := BuildFileSection2_2(filePath, prefix, fileNumber)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/builder/builder2v2/build_package.go b/builder/builder2v2/build_package.go
new file mode 100644
index 0000000..7cccf63
--- /dev/null
+++ b/builder/builder2v2/build_package.go
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "runtime"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildPackageSection2_2 creates an SPDX Package (version 2.2), returning
+// that package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - pathsIgnore: slice of strings for filepaths to ignore
+func BuildPackageSection2_2(packageName string, dirRoot string, pathsIgnore []string) (*v2_2.Package, error) {
+ // build the file section first, so we'll have it available
+ // for calculating the package verification code
+ filepaths, err := utils.GetAllFilePaths(dirRoot, pathsIgnore)
+ osType := runtime.GOOS
+
+ if err != nil {
+ return nil, err
+ }
+
+ re, ok := regexp.Compile("/+")
+ if ok != nil {
+ return nil, err
+ }
+ dirRootLen := 0
+ if osType == "windows" {
+ dirRootLen = len(dirRoot)
+ }
+
+ files := []*v2_2.File{}
+ fileNumber := 0
+ for _, fp := range filepaths {
+ newFilePatch := ""
+ if osType == "windows" {
+ newFilePatch = filepath.FromSlash("." + fp[dirRootLen:])
+ } else {
+ newFilePatch = filepath.FromSlash("./" + fp)
+ }
+ newFile, err := BuildFileSection2_2(re.ReplaceAllLiteralString(newFilePatch, string(filepath.Separator)), dirRoot, fileNumber)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, newFile)
+ fileNumber++
+ }
+
+ // get the verification code
+ code, err := utils.GetVerificationCode2_2(files, "")
+ if err != nil {
+ return nil, err
+ }
+
+ // now build the package section
+ pkg := &v2_2.Package{
+ PackageName: packageName,
+ PackageSPDXIdentifier: common.ElementID(fmt.Sprintf("Package-%s", packageName)),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: code,
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{},
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: files,
+ }
+
+ return pkg, nil
+}
diff --git a/builder/builder2v2/build_package_test.go b/builder/builder2v2/build_package_test.go
new file mode 100644
index 0000000..8a21416
--- /dev/null
+++ b/builder/builder2v2/build_package_test.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Package section builder tests =====
+func TestBuilder2_2CanBuildPackageSection(t *testing.T) {
+ packageName := "project1"
+ dirRoot := "../../testdata/project1/"
+
+ wantVerificationCode := common.PackageVerificationCode{Value: "fc9ac4a370af0a471c2e52af66d6b4cf4e2ba12b"}
+
+ pkg, err := BuildPackageSection2_2(packageName, dirRoot, nil)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if pkg == nil {
+ t.Fatalf("expected non-nil Package, got nil")
+ }
+ if pkg.PackageName != "project1" {
+ t.Errorf("expected %v, got %v", "project1", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != common.ElementID("Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", pkg.PackageSPDXIdentifier)
+ }
+ if pkg.PackageDownloadLocation != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageDownloadLocation)
+ }
+ if pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, pkg.FilesAnalyzed)
+ }
+ if pkg.IsFilesAnalyzedTagPresent != true {
+ t.Errorf("expected %v, got %v", true, pkg.IsFilesAnalyzedTagPresent)
+ }
+ if pkg.PackageVerificationCode.Value != wantVerificationCode.Value {
+ t.Errorf("expected %v, got %v", wantVerificationCode, pkg.PackageVerificationCode)
+ }
+ if pkg.PackageLicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseConcluded)
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 0 {
+ t.Errorf("expected %v, got %v", 0, len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseDeclared != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageCopyrightText)
+ }
+
+ // and make sure we got the right number of files, and check the first one
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+ fileEmpty := pkg.Files[0]
+ if fileEmpty == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if fileEmpty.FileName != "./emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./emptyfile.testdata.txt", fileEmpty.FileName)
+ }
+ if fileEmpty.FileSPDXIdentifier != common.ElementID("File0") {
+ t.Errorf("expected %v, got %v", "File0", fileEmpty.FileSPDXIdentifier)
+ }
+ for _, checksum := range fileEmpty.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
+ t.Errorf("expected %v, got %v", "da39a3ee5e6b4b0d3255bfef95601890afd80709", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
+ t.Errorf("expected %v, got %v", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "d41d8cd98f00b204e9800998ecf8427e" {
+ t.Errorf("expected %v, got %v", "d41d8cd98f00b204e9800998ecf8427e", checksum.Value)
+ }
+ }
+ }
+ if fileEmpty.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseConcluded)
+ }
+ if len(fileEmpty.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(fileEmpty.LicenseInfoInFiles))
+ } else {
+ if fileEmpty.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseInfoInFiles[0])
+ }
+ }
+ if fileEmpty.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.FileCopyrightText)
+ }
+}
+
+func TestBuilder2_2CanIgnoreFiles(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../../testdata/project3/"
+ pathsIgnored := []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ }
+ pkg, err := BuildPackageSection2_2(packageName, dirRoot, pathsIgnored)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ // make sure we got the right files
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+
+ want := "./dontscan.txt"
+ got := pkg.Files[0].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep/keep.txt"
+ got = pkg.Files[1].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep.txt"
+ got = pkg.Files[2].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/dontscan.txt"
+ got = pkg.Files[3].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/keep.txt"
+ got = pkg.Files[4].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
diff --git a/builder/builder2v2/build_relationship.go b/builder/builder2v2/build_relationship.go
new file mode 100644
index 0000000..1dd8f2b
--- /dev/null
+++ b/builder/builder2v2/build_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// BuildRelationshipSection2_2 creates an SPDX Relationship (version 2.2)
+// solely for the document "DESCRIBES" package relationship, returning that
+// relationship or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+func BuildRelationshipSection2_2(packageName string) (*v2_2.Relationship, error) {
+ rln := &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", fmt.Sprintf("Package-%s", packageName)),
+ Relationship: "DESCRIBES",
+ }
+
+ return rln, nil
+}
diff --git a/builder/builder2v2/build_relationship_test.go b/builder/builder2v2/build_relationship_test.go
new file mode 100644
index 0000000..126ddbe
--- /dev/null
+++ b/builder/builder2v2/build_relationship_test.go
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Relationship section builder tests =====
+func TestBuilder2_2CanBuildRelationshipSection(t *testing.T) {
+ packageName := "project17"
+
+ rln, err := BuildRelationshipSection2_2(packageName)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if rln == nil {
+ t.Fatalf("expected non-nil relationship, got nil")
+ }
+ if rln.RefA != common.MakeDocElementID("", "DOCUMENT") {
+ t.Errorf("expected %v, got %v", "DOCUMENT", rln.RefA)
+ }
+ if rln.RefB != common.MakeDocElementID("", "Package-project17") {
+ t.Errorf("expected %v, got %v", "Package-project17", rln.RefB)
+ }
+ if rln.Relationship != "DESCRIBES" {
+ t.Errorf("expected %v, got %v", "DESCRIBES", rln.Relationship)
+ }
+
+}
diff --git a/builder/builder2v3/build_creation_info.go b/builder/builder2v3/build_creation_info.go
new file mode 100644
index 0000000..04a8e16
--- /dev/null
+++ b/builder/builder2v3/build_creation_info.go
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "time"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// BuildCreationInfoSection2_3 creates an SPDX Package (version 2.3), returning that
+// package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - code: verification code from Package
+// - namespacePrefix: prefix for DocumentNamespace (packageName and code will be added)
+// - creatorType: one of Person, Organization or Tool
+// - creator: creator string
+// - testValues: for testing only; call with nil when using in production
+func BuildCreationInfoSection2_3(creatorType string, creator string, testValues map[string]string) (*v2_3.CreationInfo, error) {
+ // build creator slices
+ creators := []common.Creator{
+ // add builder as a tool
+ {
+ Creator: "github.com/spdx/tools-golang/builder",
+ CreatorType: "Tool",
+ },
+ {
+ Creator: creator,
+ CreatorType: creatorType,
+ },
+ }
+
+ // use test Created time if passing test values
+ location, _ := time.LoadLocation("UTC")
+ locationTime := time.Now().In(location)
+ created := locationTime.Format("2006-01-02T15:04:05Z")
+ if testVal := testValues["Created"]; testVal != "" {
+ created = testVal
+ }
+
+ ci := &v2_3.CreationInfo{
+ Creators: creators,
+ Created: created,
+ }
+ return ci, nil
+}
diff --git a/builder/builder2v3/build_creation_info_test.go b/builder/builder2v3/build_creation_info_test.go
new file mode 100644
index 0000000..f1868f0
--- /dev/null
+++ b/builder/builder2v3/build_creation_info_test.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+)
+
+// ===== CreationInfo section builder tests =====
+func TestBuilder2_3CanBuildCreationInfoSection(t *testing.T) {
+ creatorType := "Organization"
+ creator := "Jane Doe LLC"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "Jane Doe LLC" {
+ t.Errorf("expected %s, got %s", "Jane Doe LLC", ci.Creators[0].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[1].Creator)
+ }
+ if ci.Created != "2018-10-20T16:48:00Z" {
+ t.Errorf("expected %s, got %s", "2018-10-20T16:48:00Z", ci.Created)
+ }
+}
+
+func TestBuilder2_3CanBuildCreationInfoSectionWithCreatorPerson(t *testing.T) {
+ creatorType := "Person"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[0].Creator)
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[1].Creator)
+ }
+}
+
+func TestBuilder2_3CanBuildCreationInfoSectionWithCreatorTool(t *testing.T) {
+ creatorType := "Tool"
+ creator := "some-other-tool-2.1"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+ if ci.Creators[1].Creator != "some-other-tool-2.1" {
+ t.Errorf("expected %s, got %s", "some-other-tool-2.1", ci.Creators[1])
+ }
+}
+
+func TestBuilder2_3CanBuildCreationInfoSectionWithInvalidPerson(t *testing.T) {
+ creatorType := "Whatever"
+ creator := "John Doe"
+ testValues := make(map[string]string)
+ testValues["Created"] = "2018-10-20T16:48:00Z"
+
+ ci, err := BuildCreationInfoSection2_3(creatorType, creator, testValues)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if ci == nil {
+ t.Fatalf("expected non-nil CreationInfo, got nil")
+ }
+ if len(ci.Creators) != 2 {
+ t.Fatalf("expected %d, got %d", 2, len(ci.Creators))
+ }
+ if ci.Creators[1].Creator != "John Doe" {
+ t.Errorf("expected %s, got %s", "John Doe", ci.Creators[1])
+ }
+ if ci.Creators[0].Creator != "github.com/spdx/tools-golang/builder" {
+ t.Errorf("expected %s, got %s", "github.com/spdx/tools-golang/builder", ci.Creators[0])
+ }
+}
diff --git a/builder/builder2v3/build_file.go b/builder/builder2v3/build_file.go
new file mode 100644
index 0000000..bce8e66
--- /dev/null
+++ b/builder/builder2v3/build_file.go
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildFileSection2_3 creates an SPDX File (version 2.3), returning that
+// file or error if any is encountered. Arguments:
+// - filePath: path to file, relative to prefix
+// - prefix: relative directory for filePath
+// - fileNumber: integer index (unique within package) to use in identifier
+func BuildFileSection2_3(filePath string, prefix string, fileNumber int) (*v2_3.File, error) {
+ // build the full file path
+ p := filepath.Join(prefix, filePath)
+
+ // make sure we can get the file and its hashes
+ ssha1, ssha256, smd5, err := utils.GetHashesForFilePath(p)
+ if err != nil {
+ return nil, err
+ }
+
+ // build the identifier
+ i := fmt.Sprintf("File%d", fileNumber)
+
+ // now build the File section
+ f := &v2_3.File{
+ FileName: filePath,
+ FileSPDXIdentifier: common.ElementID(i),
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: ssha1,
+ },
+ {
+ Algorithm: common.SHA256,
+ Value: ssha256,
+ },
+ {
+ Algorithm: common.MD5,
+ Value: smd5,
+ },
+ },
+ LicenseConcluded: "NOASSERTION",
+ LicenseInfoInFiles: []string{"NOASSERTION"},
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ return f, nil
+}
diff --git a/builder/builder2v3/build_file_test.go b/builder/builder2v3/build_file_test.go
new file mode 100644
index 0000000..c3eaee6
--- /dev/null
+++ b/builder/builder2v3/build_file_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== File section builder tests =====
+func TestBuilder2_3CanBuildFileSection(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "../../testdata/project1/"
+ fileNumber := 17
+
+ file1, err := BuildFileSection2_3(filePath, prefix, fileNumber)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if file1 == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if file1.FileName != "/file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/file1.testdata.txt", file1.FileName)
+ }
+ if file1.FileSPDXIdentifier != common.ElementID("File17") {
+ t.Errorf("expected %v, got %v", "File17", file1.FileSPDXIdentifier)
+ }
+
+ for _, checksum := range file1.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", checksum.Value)
+ }
+ }
+ }
+
+ if file1.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseConcluded)
+ }
+ if len(file1.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(file1.LicenseInfoInFiles))
+ } else {
+ if file1.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.LicenseInfoInFiles[0])
+ }
+ }
+ if file1.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", file1.FileCopyrightText)
+ }
+
+}
+
+func TestBuilder2_3BuildFileSectionFailsForInvalidFilePath(t *testing.T) {
+ filePath := "/file1.testdata.txt"
+ prefix := "oops/wrong/path"
+ fileNumber := 11
+
+ _, err := BuildFileSection2_3(filePath, prefix, fileNumber)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/builder/builder2v3/build_package.go b/builder/builder2v3/build_package.go
new file mode 100644
index 0000000..f278a59
--- /dev/null
+++ b/builder/builder2v3/build_package.go
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "runtime"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// BuildPackageSection2_3 creates an SPDX Package (version 2.3), returning
+// that package or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - pathsIgnore: slice of strings for filepaths to ignore
+func BuildPackageSection2_3(packageName string, dirRoot string, pathsIgnore []string) (*v2_3.Package, error) {
+ // build the file section first, so we'll have it available
+ // for calculating the package verification code
+ filepaths, err := utils.GetAllFilePaths(dirRoot, pathsIgnore)
+ osType := runtime.GOOS
+
+ if err != nil {
+ return nil, err
+ }
+
+ re, ok := regexp.Compile("/+")
+ if ok != nil {
+ return nil, err
+ }
+ dirRootLen := 0
+ if osType == "windows" {
+ dirRootLen = len(dirRoot)
+ }
+
+ files := []*v2_3.File{}
+ fileNumber := 0
+ for _, fp := range filepaths {
+ newFilePatch := ""
+ if osType == "windows" {
+ newFilePatch = filepath.FromSlash("." + fp[dirRootLen:])
+ } else {
+ newFilePatch = filepath.FromSlash("./" + fp)
+ }
+ newFile, err := BuildFileSection2_3(re.ReplaceAllLiteralString(newFilePatch, string(filepath.Separator)), dirRoot, fileNumber)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, newFile)
+ fileNumber++
+ }
+
+ // get the verification code
+ code, err := utils.GetVerificationCode2_3(files, "")
+ if err != nil {
+ return nil, err
+ }
+
+ // now build the package section
+ pkg := &v2_3.Package{
+ PackageName: packageName,
+ PackageSPDXIdentifier: common.ElementID(fmt.Sprintf("Package-%s", packageName)),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ PackageVerificationCode: &code,
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{},
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: files,
+ }
+
+ return pkg, nil
+}
diff --git a/builder/builder2v3/build_package_test.go b/builder/builder2v3/build_package_test.go
new file mode 100644
index 0000000..873e3d5
--- /dev/null
+++ b/builder/builder2v3/build_package_test.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Package section builder tests =====
+func TestBuilder2_3CanBuildPackageSection(t *testing.T) {
+ packageName := "project1"
+ dirRoot := "../../testdata/project1/"
+
+ wantVerificationCode := common.PackageVerificationCode{Value: "fc9ac4a370af0a471c2e52af66d6b4cf4e2ba12b"}
+
+ pkg, err := BuildPackageSection2_3(packageName, dirRoot, nil)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if pkg == nil {
+ t.Fatalf("expected non-nil Package, got nil")
+ }
+ if pkg.PackageName != "project1" {
+ t.Errorf("expected %v, got %v", "project1", pkg.PackageName)
+ }
+ if pkg.PackageSPDXIdentifier != common.ElementID("Package-project1") {
+ t.Errorf("expected %v, got %v", "Package-project1", pkg.PackageSPDXIdentifier)
+ }
+ if pkg.PackageDownloadLocation != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageDownloadLocation)
+ }
+ if pkg.FilesAnalyzed != true {
+ t.Errorf("expected %v, got %v", true, pkg.FilesAnalyzed)
+ }
+ if pkg.IsFilesAnalyzedTagPresent != true {
+ t.Errorf("expected %v, got %v", true, pkg.IsFilesAnalyzedTagPresent)
+ }
+ if pkg.PackageVerificationCode.Value != wantVerificationCode.Value {
+ t.Errorf("expected %v, got %v", wantVerificationCode, pkg.PackageVerificationCode)
+ }
+ if pkg.PackageLicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseConcluded)
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 0 {
+ t.Errorf("expected %v, got %v", 0, len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseDeclared != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageLicenseDeclared)
+ }
+ if pkg.PackageCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", pkg.PackageCopyrightText)
+ }
+
+ // and make sure we got the right number of files, and check the first one
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+ fileEmpty := pkg.Files[0]
+ if fileEmpty == nil {
+ t.Fatalf("expected non-nil file, got nil")
+ }
+ if fileEmpty.FileName != "./emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "./emptyfile.testdata.txt", fileEmpty.FileName)
+ }
+ if fileEmpty.FileSPDXIdentifier != common.ElementID("File0") {
+ t.Errorf("expected %v, got %v", "File0", fileEmpty.FileSPDXIdentifier)
+ }
+ for _, checksum := range fileEmpty.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
+ t.Errorf("expected %v, got %v", "da39a3ee5e6b4b0d3255bfef95601890afd80709", checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
+ t.Errorf("expected %v, got %v", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != "d41d8cd98f00b204e9800998ecf8427e" {
+ t.Errorf("expected %v, got %v", "d41d8cd98f00b204e9800998ecf8427e", checksum.Value)
+ }
+ }
+ }
+ if fileEmpty.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseConcluded)
+ }
+ if len(fileEmpty.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected %v, got %v", 1, len(fileEmpty.LicenseInfoInFiles))
+ } else {
+ if fileEmpty.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.LicenseInfoInFiles[0])
+ }
+ }
+ if fileEmpty.FileCopyrightText != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileEmpty.FileCopyrightText)
+ }
+}
+
+func TestBuilder2_3CanIgnoreFiles(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../../testdata/project3/"
+ pathsIgnored := []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ }
+ pkg, err := BuildPackageSection2_3(packageName, dirRoot, pathsIgnored)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ // make sure we got the right files
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil pkg.Files, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected %d, got %d", 5, len(pkg.Files))
+ }
+
+ want := "./dontscan.txt"
+ got := pkg.Files[0].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep/keep.txt"
+ got = pkg.Files[1].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./keep.txt"
+ got = pkg.Files[2].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/dontscan.txt"
+ got = pkg.Files[3].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ want = "./subdir/keep/keep.txt"
+ got = pkg.Files[4].FileName
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
diff --git a/builder/builder2v3/build_relationship.go b/builder/builder2v3/build_relationship.go
new file mode 100644
index 0000000..45ce5be
--- /dev/null
+++ b/builder/builder2v3/build_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// BuildRelationshipSection2_3 creates an SPDX Relationship (version 2.3)
+// solely for the document "DESCRIBES" package relationship, returning that
+// relationship or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+func BuildRelationshipSection2_3(packageName string) (*v2_3.Relationship, error) {
+ rln := &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", fmt.Sprintf("Package-%s", packageName)),
+ Relationship: "DESCRIBES",
+ }
+
+ return rln, nil
+}
diff --git a/builder/builder2v3/build_relationship_test.go b/builder/builder2v3/build_relationship_test.go
new file mode 100644
index 0000000..cbf27a7
--- /dev/null
+++ b/builder/builder2v3/build_relationship_test.go
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package builder2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// ===== Relationship section builder tests =====
+func TestBuilder2_3CanBuildRelationshipSection(t *testing.T) {
+ packageName := "project17"
+
+ rln, err := BuildRelationshipSection2_3(packageName)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if rln == nil {
+ t.Fatalf("expected non-nil relationship, got nil")
+ }
+ if rln.RefA != common.MakeDocElementID("", "DOCUMENT") {
+ t.Errorf("expected %v, got %v", "DOCUMENT", rln.RefA)
+ }
+ if rln.RefB != common.MakeDocElementID("", "Package-project17") {
+ t.Errorf("expected %v, got %v", "Package-project17", rln.RefB)
+ }
+ if rln.Relationship != "DESCRIBES" {
+ t.Errorf("expected %v, got %v", "DESCRIBES", rln.Relationship)
+ }
+
+}
diff --git a/docs/builder-assumptions.md b/docs/builder-assumptions.md
new file mode 100644
index 0000000..af08587
--- /dev/null
+++ b/docs/builder-assumptions.md
@@ -0,0 +1,6 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+The Document builder in `package builder` makes the following assumptions:
+
+- Symbolic links will be ignored and will not be included in the Document's Files (see
+ https://github.com/swinslow/spdx-go/issues/13).
diff --git a/docs/idsearcher-assumptions.md b/docs/idsearcher-assumptions.md
new file mode 100644
index 0000000..32b75e9
--- /dev/null
+++ b/docs/idsearcher-assumptions.md
@@ -0,0 +1,13 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+The short-form ID searcher in `package idsearcher` makes the following assumptions:
+
+- The searcher uses bufio.Scanner to scan the contents of each file, line by
+ line. If it encounters a line that is too large to fit within the buffer, it
+ will stop scanning that file. As a result, it will currently only pick up
+ short-form IDs that occur prior to such a line.
+
+- For PackageLicenseInfoFromFiles (in Package) and LicenseInfoInFile (in File),
+ an exception should be treated as a separate "license". For example, in the
+ expression `GPL-2.0-only WITH Classpath-exception-2.0`, each of `GPL-2.0-only`
+ and `Classpath-exception-2.0` will be listed separately.
diff --git a/docs/jsonloader.md b/docs/jsonloader.md
new file mode 100644
index 0000000..3d6b1b1
--- /dev/null
+++ b/docs/jsonloader.md
@@ -0,0 +1,23 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+## Usage
+
+A json.Unmarshal function on the v2_2.Document struct is defined so that when the JSON is unmarshalled, the function is called and the JSON can be processed in a custom way. Then a new map[string]interface{} is defined which temporarily holds the unmarshalled JSON. The map is then parsed into the v2_2.Document using functions defined for each different section.
+
+JSON => map[string]interface{} => v2_2.Document
+
+## Some Key Points
+
+- The packages have a property "hasFiles" defined in the schema which is an array of the SPDX Identifiers of the files of that package. The parser first parses all the files into the UnpackagedFiles map of the document and then when it parses the Packages, it removes the respective files from the UnpackagedFiles map and places them inside the Files map of the corresponding package.
+
+- The snippets have a property "snippetFromFile" which has the SPDX identifier of the file to which the snippet is related. Thus the snippets require the files to be parsed before them. Then the snippets are parsed one by one and inserted into the respective files using this property.
+
+## Assumptions
+
+The json file loader in `package jsonloader` makes the following assumptions:
+
+### Order of appearance of the properties
+* The parser does not make any assumptions based on the order in which the properties appear.
+
+### Annotations
+* The JSON SPDX schema does not define the SPDX Identifier property for the annotation object. The parser assumes the SPDX Identifier of the parent property of the currently-being-parsed annotation array to be the SPDX Identifer for all the annotation objects of that array.
diff --git a/docs/jsonsaver.md b/docs/jsonsaver.md
new file mode 100644
index 0000000..bd6f95f
--- /dev/null
+++ b/docs/jsonsaver.md
@@ -0,0 +1,28 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+## Working
+
+The SPDX document is converted to map[string]interface{} and then the entire map is converted to JSON using a single json.MarshalIndent function call. The saver uses temporary memory to store all the files (Packaged and Unpackaged) together in a single data structure in order to comply with the JSON schema defined by SPDX.
+
+v2_2.Document => map[string]interface{} => JSON
+
+## Some Key Points
+
+- The packages have a property "hasFiles" defined in the schema which is an array of the SPDX Identifiers of the files of that package. The saver iterates through the files of a package and inserts all the SPDX Identifiers of the files in the "hasFiles" array. In addition it adds each file to a temporary storage map to store all the files of the entire document at a single place.
+
+- The files require the packages to be saved before them in order to ensure that the packaged files are added to the temporary storage before the files are saved.
+
+- The snippets are saved after the files and a property "snippetFromFile" identifies the file containing each snippet.
+
+## Assumptions
+
+The json file loader in `package jsonsaver` makes the following assumptions:
+
+### Order of appearance of the properties
+* The saver does not make any pre-assumptions based on the order in which the properties are saved.
+
+### Annotations
+* The JSON SPDX schema does not define the SPDX Identifier property for the annotation object. The saver inserts the annotation inside the element whose SPDX Identifier matches the annotation's SPDX Identifier.
+
+### Indentation
+* The jsonsaver uses the MarshalIndent function with "" as the prefix and "\t" as the indent character, passed as function parameters.
diff --git a/docs/licensediff-assumptions.md b/docs/licensediff-assumptions.md
new file mode 100644
index 0000000..d587791
--- /dev/null
+++ b/docs/licensediff-assumptions.md
@@ -0,0 +1,7 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+The License diff tool in `package licensediff` makes the following assumptions:
+
+- In any single Package, a given filename will only appear once. This may or may
+ not be required by the SPDX spec, but it's kind of implicit in being able to
+ create a diff indexed by filename. \ No newline at end of file
diff --git a/docs/tvloader-assumptions.md b/docs/tvloader-assumptions.md
new file mode 100644
index 0000000..eb84d49
--- /dev/null
+++ b/docs/tvloader-assumptions.md
@@ -0,0 +1,34 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+The tag-value file loader in `package tvloader` makes the following assumptions:
+
+Document Creation Info
+----------------------
+* The Document Creation Info section will always come first, and be completed
+ first. Although the spec may not make this explicit, it appears that this is
+ the intended format. Unless it comes first, the parser will not be able to
+ confirm what version of the SPDX spec is being used. And, "SPDXID:" tags are
+ used for not just the Document Creation Info section but also for others (e.g.
+ Packages, Files).
+
+Relationship
+------------
+* Relationship sections will begin with the "Relationship" tag.
+
+Annotation
+----------
+* Annotation sections will begin with the "Annotator" tag.
+
+Other License Info
+------------------
+* Other License sections will begin with the "LicenseID" tag.
+
+* Any Other License section, if present, will come later than the Document
+ Creation Info section and after any Package, File and Snippet sections.
+
+Review
+------
+* Review sections will begin with the "Reviewer" tag.
+
+* Any Review section, if present, will come later than the Document Creation
+ Info section and after any Package, File, Snippet, and Other License sections.
diff --git a/docs/tvsaver-assumptions.md b/docs/tvsaver-assumptions.md
new file mode 100644
index 0000000..f210a8a
--- /dev/null
+++ b/docs/tvsaver-assumptions.md
@@ -0,0 +1,14 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+The tag-value file saver in `package tvsaver` makes the following assumptions:
+
+Document Creation Info
+----------------------
+* Mandatory fields will be treated the same way as optional fields; if they are
+ set to the empty string, they will be omitted. Thus, an invalid Creation Info
+ section (e.g. one that doesn't include a correct SPDXVersion field) will
+ result in outputting an invalid Creation Info section.
+
+Relationship
+------------
+* Same comment as above re: optional fields, for RelationshipComment.
diff --git a/examples/1-load/example_load.go b/examples/1-load/example_load.go
new file mode 100644
index 0000000..328d349
--- /dev/null
+++ b/examples/1-load/example_load.go
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *tvloader*, *spdx*
+
+// This example demonstrates loading an SPDX tag-value file from disk into
+// memory, and printing some of its contents to standard output.
+// Run project: go run example_load.go ../sample-docs/tv/hello.spdx
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/spdxlib"
+ "github.com/spdx/tools-golang/tvloader"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 2 {
+ fmt.Printf("Usage: %v <spdx-file-in>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 tag-value file <spdx-file-in>, and\n")
+ fmt.Printf(" print a portion of its contents.\n")
+ return
+ }
+
+ // open the SPDX file
+ filename := args[1]
+ r, err := os.Open(filename)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", filename, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a tag-value file, version 2.2
+ doc, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", filename, err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n\n", filename)
+
+ // we can now take a look at its contents via the various data
+ // structures representing the SPDX document's sections.
+
+ // print the struct containing the SPDX file's Creation Info section data
+ fmt.Printf("==============\n")
+ fmt.Printf("Creation info:\n")
+ fmt.Printf("==============\n")
+ fmt.Printf("%#v\n\n", doc.CreationInfo)
+
+ // check whether the SPDX file has at least one package that it describes
+ pkgIDs, err := spdxlib.GetDescribedPackageIDs2_2(doc)
+ if err != nil {
+ fmt.Printf("Unable to get describe packages from SPDX document: %v\n", err)
+ return
+ }
+
+ if len(pkgIDs) == 0 {
+ return
+ }
+
+ // it does, so we'll go through each one
+ for _, pkg := range doc.Packages {
+ var documentDescribesPackage bool
+ for _, describedPackageID := range pkgIDs {
+ if pkg.PackageSPDXIdentifier == describedPackageID {
+ documentDescribesPackage = true
+ break
+ }
+ }
+
+ if !documentDescribesPackage {
+ continue
+ }
+
+ pkgID := pkg.PackageSPDXIdentifier
+
+ // check whether the package had its files analyzed
+ if !pkg.FilesAnalyzed {
+ fmt.Printf("Package %s (%s) had FilesAnalyzed: false\n", string(pkgID), pkg.PackageName)
+ continue
+ }
+
+ // also check whether the package has any files present
+ if pkg.Files == nil || len(pkg.Files) < 1 {
+ fmt.Printf("Package %s (%s) has no Files\n", string(pkgID), pkg.PackageName)
+ continue
+ }
+
+ // if we got here, there's at least one file
+ // print the filename and license info for the first 50
+ fmt.Printf("============================\n")
+ fmt.Printf("Package %s (%s)\n", string(pkgID), pkg.PackageName)
+ fmt.Printf("File info (up to first 50):\n")
+ i := 1
+ for _, f := range pkg.Files {
+ // note that these will be in random order, since we're pulling
+ // from a map. if we care about order, we should first pull the
+ // IDs into a slice, sort it, and then print the ordered files.
+ fmt.Printf("- File %d: %s\n", i, f.FileName)
+ fmt.Printf(" License from file: %v\n", f.LicenseInfoInFiles)
+ fmt.Printf(" License concluded: %v\n", f.LicenseConcluded)
+ i++
+ if i > 50 {
+ break
+ }
+ }
+ }
+}
diff --git a/examples/10-jsonloader/example_json_loader.go b/examples/10-jsonloader/example_json_loader.go
new file mode 100644
index 0000000..1ccf4d0
--- /dev/null
+++ b/examples/10-jsonloader/example_json_loader.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *json*
+
+// This example demonstrates loading an SPDX JSON document from disk into memory,
+// and then logging some attributes to the console.
+// Run project: go run example_json_loader.go ../sample-docs/json/SPDXJSONExample-v2.2.spdx.json
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ spdx_json "github.com/spdx/tools-golang/json"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 2 {
+ fmt.Printf("Usage: %v <json-file-in>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 JSON file <spdx-file-in>, and\n")
+ fmt.Printf(" print portions of its creation info data.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a json file, version 2.2
+ doc, err := spdx_json.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", args[1], err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", args[1])
+
+ fmt.Println(strings.Repeat("=", 80))
+ fmt.Println("Some Attributes of the Document:")
+ fmt.Printf("Document Name: %s\n", doc.DocumentName)
+ fmt.Printf("DataLicense: %s\n", doc.DataLicense)
+ fmt.Printf("Document Namespace: %s\n", doc.DocumentNamespace)
+ fmt.Printf("SPDX Version: %s\n", doc.SPDXVersion)
+ fmt.Println(strings.Repeat("=", 80))
+}
diff --git a/examples/11-yamltotv/exampleyamltotv.go b/examples/11-yamltotv/exampleyamltotv.go
new file mode 100644
index 0000000..d14201c
--- /dev/null
+++ b/examples/11-yamltotv/exampleyamltotv.go
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *yaml* *tvsaver*
+
+// This example demonstrates loading an SPDX tag-value file from disk into memory,
+// and re-saving it to a different file on disk.
+// Run project: go run exampleyamltotv.go ../sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml test.spdx
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/tvsaver"
+ spdx_yaml "github.com/spdx/tools-golang/yaml"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 3 {
+ fmt.Printf("Usage: %v <yaml-file-in> <spdx-file-out>\n", args[0])
+ fmt.Printf(" Load YAML file <yaml-file-in>, and\n")
+ fmt.Printf(" save it out to <spdx-file-out>.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a YAML file
+ doc, err := spdx_yaml.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", fileIn, err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", fileIn)
+
+ // we can now save it back to disk, using spdx_yaml, but tvsaver work also.
+
+ // create a new file for writing
+ fileOut := args[2]
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ // try to save the document to disk as an SPDX tag-value file, version 2.2
+ err = tvsaver.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ // it worked
+ fmt.Printf("Successfully saved %s\n", fileOut)
+}
diff --git a/examples/12-tvtoyaml/exampletvtoyaml.go b/examples/12-tvtoyaml/exampletvtoyaml.go
new file mode 100644
index 0000000..19abde7
--- /dev/null
+++ b/examples/12-tvtoyaml/exampletvtoyaml.go
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *tvloader*, *yaml*
+
+// This example demonstrates loading an SPDX tag-value file from disk into memory,
+// and re-saving it to a different json file on disk.
+// Run project: go run exampletvtoyaml.go ../sample-docs/tv/hello.spdx example.yaml
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/tvloader"
+ spdx_yaml "github.com/spdx/tools-golang/yaml"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 3 {
+ fmt.Printf("Usage: %v <spdx-file-in> <yaml-file-out>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 tag-value file <spdx-file-in>, and\n")
+ fmt.Printf(" save it out to <yaml-file-out>.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a tag-value file, version 2.2
+ doc, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", args[1], err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", args[1])
+
+ // we can now save it back to disk, using yaml.
+
+ // create a new file for writing
+ fileOut := args[2]
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ // try to save the document to disk as an YAML file
+ err = spdx_yaml.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ // it worked
+ fmt.Printf("Successfully saved %s\n", fileOut)
+}
diff --git a/examples/13-yamlloader/exampleYAMLLoader.go b/examples/13-yamlloader/exampleYAMLLoader.go
new file mode 100644
index 0000000..a032ece
--- /dev/null
+++ b/examples/13-yamlloader/exampleYAMLLoader.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *yaml*
+
+// This example demonstrates loading an SPDX YAML document from disk into memory,
+// and then logging some attributes to the console.
+// Run project: go run exampleYAMLLoader.go ../sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ spdx_yaml "github.com/spdx/tools-golang/yaml"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 2 {
+ fmt.Printf("Usage: %v <json-file-in>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 JSON file <spdx-file-in>, and\n")
+ fmt.Printf(" print portions of its creation info data.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a json file, version 2.2
+ doc, err := spdx_yaml.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", args[1], err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", args[1])
+
+ fmt.Println(strings.Repeat("=", 80))
+ fmt.Println("Some Attributes of the Document:")
+ fmt.Printf("Document Name: %s\n", doc.DocumentName)
+ fmt.Printf("DataLicense: %s\n", doc.DataLicense)
+ fmt.Printf("Document Namespace: %s\n", doc.DocumentNamespace)
+ fmt.Printf("SPDX Version: %s\n", doc.SPDXVersion)
+ fmt.Println(strings.Repeat("=", 80))
+}
diff --git a/examples/2-load-save/example_load_save.go b/examples/2-load-save/example_load_save.go
new file mode 100644
index 0000000..7d23403
--- /dev/null
+++ b/examples/2-load-save/example_load_save.go
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *tvloader*, *tvsaver*
+
+// This example demonstrates loading an SPDX tag-value file from disk into memory,
+// and re-saving it to a different file on disk.
+// Run project: go run example_load_save.go ../sample-docs/tv/hello.spdx test.spdx
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/tvloader"
+ "github.com/spdx/tools-golang/tvsaver"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 3 {
+ fmt.Printf("Usage: %v <spdx-file-in> <spdx-file-out>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 tag-value file <spdx-file-in>, and\n")
+ fmt.Printf(" save it out to <spdx-file-out>.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a tag-value file, version 2.2
+ doc, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", fileIn, err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", fileIn)
+
+ // we can now save it back to disk, using tvsaver.
+
+ // create a new file for writing
+ fileOut := args[2]
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ // try to save the document to disk as an SPDX tag-value file, version 2.2
+ err = tvsaver.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ // it worked
+ fmt.Printf("Successfully saved %s\n", fileOut)
+}
diff --git a/examples/3-build/example_build.go b/examples/3-build/example_build.go
new file mode 100644
index 0000000..c6051cc
--- /dev/null
+++ b/examples/3-build/example_build.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *builder*, *tvsaver*
+
+// This example demonstrates building an 'empty' SPDX document in memory that
+// corresponds to a given directory's contents, including all files with their
+// hashes and the package's verification code, and saving the document to disk.
+// Run project: go run example_build.go project2 ../../testdata/project2 test.spdx
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/builder"
+ "github.com/spdx/tools-golang/tvsaver"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 4 {
+ fmt.Printf("Usage: %v <package-name> <package-root-dir> <spdx-file-out>\n", args[0])
+ fmt.Printf(" Build a SPDX 2.2 document with one package called <package-name>;\n")
+ fmt.Printf(" create files with hashes corresponding to the files in <package-root-dir>;\n")
+ fmt.Printf(" and save it out as a tag-value file to <spdx-file-out>.\n")
+ return
+ }
+
+ // get the command-line arguments
+ packageName := args[1]
+ packageRootDir := args[2]
+ fileOut := args[3]
+
+ // to use the SPDX builder package, the first step is to define a
+ // builder.Config2_2 struct. this config data can be reused, in case you
+ // are building SPDX documents for several directories in sequence.
+ config := &builder.Config2_2{
+
+ // NamespacePrefix is a prefix that will be used to populate the
+ // mandatory DocumentNamespace field in the Creation Info section.
+ // Because it needs to be unique, the value that will be filled in
+ // for the document will have the package name and verification code
+ // appended to this prefix.
+ NamespacePrefix: "https://example.com/whatever/testdata-",
+
+ // CreatorType will be used for the first part of the Creator field
+ // in the Creation Info section. Per the SPDX spec, it can be
+ // "Person", "Organization" or "Tool".
+ CreatorType: "Person",
+
+ // Creator will be used for the second part of the Creator field in
+ // the Creation Info section.
+ Creator: "Jane Doe",
+
+ // note that builder will also add the following, in addition to the
+ // Creator defined above:
+ // Creator: Tool: github.com/spdx/tools-golang/builder
+
+ // Finally, you can define one or more paths that should be ignored
+ // when walking through the directory. This is intended to omit files
+ // that are located within the package's directory, but which should
+ // be omitted from the SPDX document.
+ PathsIgnored: []string{
+
+ // ignore all files in the .git/ directory at the package root
+ "/.git/",
+
+ // ignore all files in all __pycache__/ directories, anywhere
+ // within the package directory tree
+ "**/__pycache__/",
+
+ // ignore the file with this specific path relative to the
+ // package root
+ "/.ignorefile",
+
+ // or ignore all files with this filename, anywhere within the
+ // package directory tree
+ "**/.DS_Store",
+ },
+ }
+
+ // now, when we actually ask builder to walk through a directory and
+ // build an SPDX document, we need to give it three things:
+ // - what to name the package; and
+ // - where the directory is located on disk; and
+ // - the config object we just defined.
+ doc, err := builder.Build2_2(packageName, packageRootDir, config)
+ if err != nil {
+ fmt.Printf("Error while building document: %v\n", err)
+ return
+ }
+
+ // if we got here, the document has been created.
+ // all license info is marked as NOASSERTION, but file hashes and
+ // the package verification code have been filled in appropriately.
+ fmt.Printf("Successfully created document for package %s\n", packageName)
+
+ // we can now save it to disk, using tvsaver.
+
+ // create a new file for writing
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v\n", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ err = tvsaver.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ fmt.Printf("Successfully saved %v\n", fileOut)
+}
diff --git a/examples/4-search/example_search.go b/examples/4-search/example_search.go
new file mode 100644
index 0000000..52f8b07
--- /dev/null
+++ b/examples/4-search/example_search.go
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *idsearcher*, *tvsaver*
+
+// This example demonstrates building an SPDX document for a directory's
+// contents (implicitly using *builder*); searching through that directory for
+// [SPDX short-form IDs](https://spdx.org/ids/); filling those IDs into the
+// document's Package and File license fields; and saving the resulting document
+// to disk.
+// Run project: go run example_search.go project2 ../../testdata/project2/folder test.spdx
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/idsearcher"
+
+ "github.com/spdx/tools-golang/tvsaver"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 4 {
+ fmt.Printf("Usage: %v <package-name> <package-root-dir> <spdx-file-out>\n", args[0])
+ fmt.Printf(" Build a SPDX 2.2 document with one package called <package-name>;\n")
+ fmt.Printf(" create files with hashes corresponding to the files in <package-root-dir>;\n")
+ fmt.Printf(" search for SPDX short-form IDs, and use them to fill in license data\n")
+ fmt.Printf(" where possible; and save it out as a tag-value file to <spdx-file-out>.\n")
+ return
+ }
+
+ // get the command-line arguments
+ packageName := args[1]
+ packageRootDir := args[2]
+ fileOut := args[3]
+
+ // to use the SPDX idsearcher package, the first step is to define a
+ // idsearcher.Config2_2 struct. this config data can be reused, in case you
+ // are building SPDX documents for several directories in sequence.
+ config := &idsearcher.Config2_2{
+
+ // NamespacePrefix is a prefix that will be used to populate the
+ // mandatory DocumentNamespace field in the Creation Info section.
+ // Because it needs to be unique, the value that will be filled in
+ // for the document will have the package name and verification code
+ // appended to this prefix.
+ NamespacePrefix: "https://example.com/whatever/testdata-",
+
+ // CreatorType and Creator, from builder.Config2_2, are not needed for
+ // idsearcher.Config2_2. Because it is automated and doesn't assume
+ // further review, the following two Creator fields are filled in:
+ // Creator: Tool: github.com/spdx/tools-golang/builder
+ // Creator: Tool: github.com/spdx/tools-golang/idsearcher
+
+ // You can define one or more paths that should be ignored
+ // when walking through the directory. This is intended to omit files
+ // that are located within the package's directory, but which should
+ // be omitted from the SPDX document.
+ // This is directly passed through to builder, and uses the same
+ // format as shown in examples/3-build/example_build.go.
+ BuilderPathsIgnored: []string{
+
+ // ignore all files in the .git/ directory at the package root
+ "/.git/",
+
+ // ignore all files in all __pycache__/ directories, anywhere
+ // within the package directory tree
+ "**/__pycache__/",
+
+ // ignore the file with this specific path relative to the
+ // package root
+ "/.ignorefile",
+
+ // or ignore all files with this filename, anywhere within the
+ // package directory tree
+ "**/.DS_Store",
+ },
+
+ // Finally, SearcherPathsIgnored lists certain paths that should not be
+ // searched by idsearcher, even if those paths have Files present (and
+ // had files filled in by builder). This is useful, for instance, if
+ // your project has some directories or files with
+ // "SPDX-License-Identifier:" tags, but for one reason or another you
+ // want to exclude those files' tags from being picked up by the
+ // searcher.
+ // SearcherPathsIgnored uses the same format as BuilderPathsIgnored.
+ SearcherPathsIgnored: []string{
+
+ // Example for the Linux kernel: ignore the documentation file
+ // which explains how to use SPDX short-form IDs (and therefore
+ // has a bunch of "SPDX-License-Identifier:" tags that we wouldn't
+ // want to pick up).
+ "/Documentation/process/license-rules.rst",
+
+ // Similar example for the Linux kernel: ignore all files in the
+ // /LICENSES/ directory.
+ "/LICENSES/",
+ },
+ }
+
+ // now, when we actually ask idsearcher to walk through a directory and
+ // build an SPDX document, we need to give it three things:
+ // - what to name the package; and
+ // - where the directory is located on disk; and
+ // - the config object we just defined.
+ // these are the same arguments needed for builder, and in fact they get
+ // passed through to builder (with the relevant data from the config
+ // object extracted behind the scenes).
+ doc, err := idsearcher.BuildIDsDocument2_2(packageName, packageRootDir, config)
+ if err != nil {
+ fmt.Printf("Error while building document: %v\n", err)
+ return
+ }
+
+ // if we got here, the document has been created.
+ // all file hashes and the package verification code have been filled in
+ // appropriately by builder.
+ // And, all files with "SPDX-License-Identifier:" tags have had their
+ // licenses extracted into LicenseInfoInFiles and LicenseConcluded for
+ // each file by idsearcher. The PackageLicenseInfoFromFiles field will
+ // also be filled in with all license identifiers.
+ fmt.Printf("Successfully created document and searched for IDs for package %s\n", packageName)
+
+ // NOTE that BuildIDsDocument does NOT do any validation of the license
+ // identifiers, to confirm that they are e.g. on the SPDX License List
+ // or in other appropriate format (e.g., LicenseRef-...)
+
+ // we can now save it to disk, using tvsaver.
+
+ // create a new file for writing
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v\n", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ err = tvsaver.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ fmt.Printf("Successfully saved %v\n", fileOut)
+}
diff --git a/examples/5-report/example_report.go b/examples/5-report/example_report.go
new file mode 100644
index 0000000..1197547
--- /dev/null
+++ b/examples/5-report/example_report.go
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *reporter*, *tvloader*
+
+// This example demonstrates loading an SPDX tag-value file from disk into memory,
+// generating a basic report listing counts of the concluded licenses for its
+// files, and printing the report to standard output.
+// Run project: go run example_report.go ../sample-docs/tv/hello.spdx
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/reporter"
+ "github.com/spdx/tools-golang/spdxlib"
+ "github.com/spdx/tools-golang/tvloader"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 2 {
+ fmt.Printf("Usage: %v <spdx-file-in>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 tag-value file <spdx-file-in>, and\n")
+ fmt.Printf(" generate and print a report of its concluded licenses.\n")
+ return
+ }
+
+ // open the SPDX file
+ filename := args[1]
+ r, err := os.Open(filename)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", filename, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a tag-value file, version 2.2
+ doc, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", filename, err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n\n", filename)
+
+ // check whether the SPDX file has at least one package that it describes
+ pkgIDs, err := spdxlib.GetDescribedPackageIDs2_2(doc)
+ if err != nil {
+ fmt.Printf("Unable to get describe packages from SPDX document: %v\n", err)
+ return
+ }
+
+ if len(pkgIDs) == 0 {
+ return
+ }
+
+ // it does, so we'll go through each one
+ for _, pkg := range doc.Packages {
+ var documentDescribesPackage bool
+ for _, describedPackageID := range pkgIDs {
+ if pkg.PackageSPDXIdentifier == describedPackageID {
+ documentDescribesPackage = true
+ break
+ }
+ }
+
+ if !documentDescribesPackage {
+ continue
+ }
+
+ pkgID := pkg.PackageSPDXIdentifier
+
+ // check whether the package had its files analyzed
+ if !pkg.FilesAnalyzed {
+ fmt.Printf("Package %s (%s) had FilesAnalyzed: false\n", string(pkgID), pkg.PackageName)
+ return
+ }
+
+ // also check whether the package has any files present
+ if pkg.Files == nil || len(pkg.Files) < 1 {
+ fmt.Printf("Package %s (%s) has no Files\n", string(pkgID), pkg.PackageName)
+ return
+ }
+
+ // if we got here, there's at least one file
+ // generate and print a report of the Package's Files' LicenseConcluded
+ // values, sorted by # of occurrences
+ fmt.Printf("============================\n")
+ fmt.Printf("Package %s (%s)\n", string(pkgID), pkg.PackageName)
+ err = reporter.Generate2_2(pkg, os.Stdout)
+ if err != nil {
+ fmt.Printf("Error while generating report: %v\n", err)
+ }
+ }
+}
diff --git a/examples/6-licensediff/example_licensediff.go b/examples/6-licensediff/example_licensediff.go
new file mode 100644
index 0000000..39c369d
--- /dev/null
+++ b/examples/6-licensediff/example_licensediff.go
@@ -0,0 +1,169 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *licensediff*, *tvloader*
+
+// This example demonstrates loading two SPDX tag-value files from disk into
+// memory, and generating a diff of the concluded licenses for Files in
+// Packages with matching IDs in each document.
+// This is generally only useful when run with two SPDX documents that
+// describe licenses for subsequent versions of the same set of files, AND if
+// they have the same identifier in both documents.
+// Run project: go run example_licensediff.go ../sample-docs/tv/hello.spdx ../sample-docs/tv/hello-modified.spdx
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spdx/tools-golang/licensediff"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdxlib"
+ "github.com/spdx/tools-golang/tvloader"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 3 {
+ fmt.Printf("Usage: %v <spdx-file-first> <spdx-file-second>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 tag-value files <spdx-file-first> and <spdx-file-second>,\n")
+ fmt.Printf(" run a diff between their concluded licenses, and print basic results.\n")
+ return
+ }
+
+ // open the first SPDX file
+ filenameFirst := args[1]
+ r, err := os.Open(filenameFirst)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", filenameFirst, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the first SPDX file's contents as a tag-value file, version 2.2
+ docFirst, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", filenameFirst, err)
+ return
+ }
+ // check whether the SPDX file has at least one package that it describes
+ pkgIDsFirst, err := spdxlib.GetDescribedPackageIDs2_2(docFirst)
+ if err != nil {
+ fmt.Printf("Unable to get describe packages from first SPDX document: %v\n", err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded first SPDX file %s\n", filenameFirst)
+
+ // open the second SPDX file
+ filenameSecond := args[2]
+ r, err = os.Open(filenameSecond)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", filenameSecond, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the second SPDX file's contents as a tag-value file, version 2.2
+ docSecond, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", filenameSecond, err)
+ return
+ }
+ // check whether the SPDX file has at least one package that it describes
+ pkgIDsSecond, err := spdxlib.GetDescribedPackageIDs2_2(docSecond)
+ if err != nil {
+ fmt.Printf("Unable to get describe packages from second SPDX document: %v\n", err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded second SPDX file %s\n\n", filenameSecond)
+
+ // compare the described packages from each Document, by SPDX ID
+ // go through the first set first, report if they aren't in the second set
+ for _, pkgID := range pkgIDsFirst {
+ fmt.Printf("================================\n")
+
+ var p1, p2 *v2_2.Package
+ var okFirst, okSecond bool
+ for _, pkg := range docFirst.Packages {
+ if pkg.PackageSPDXIdentifier == pkgID {
+ okFirst = true
+ p1 = pkg
+ break
+ }
+ }
+ if !okFirst {
+ fmt.Printf("Package %s has described relationship in first document but ID not found\n", string(pkgID))
+ continue
+ }
+
+ fmt.Printf("Package %s (%s)\n", string(pkgID), p1.PackageName)
+
+ for _, pkg := range docSecond.Packages {
+ if pkg.PackageSPDXIdentifier == pkgID {
+ okSecond = true
+ p2 = pkg
+ break
+ }
+ }
+ if !okSecond {
+ fmt.Printf(" Found in first document, not found in second\n")
+ continue
+ }
+
+ // now, run a diff between the two
+ pairs, err := licensediff.MakePairs2_2(p1, p2)
+ if err != nil {
+ fmt.Printf(" Error generating licensediff pairs: %v\n", err)
+ continue
+ }
+
+ // take the pairs and turn them into a more structured results set
+ resultSet, err := licensediff.MakeResults(pairs)
+ if err != nil {
+ fmt.Printf(" Error generating licensediff results set: %v\n", err)
+ continue
+ }
+
+ // print some information about the results
+ fmt.Printf(" Files in first only: %d\n", len(resultSet.InFirstOnly))
+ fmt.Printf(" Files in second only: %d\n", len(resultSet.InSecondOnly))
+ fmt.Printf(" Files in both with different licenses: %d\n", len(resultSet.InBothChanged))
+ fmt.Printf(" Files in both with same licenses: %d\n", len(resultSet.InBothSame))
+ }
+
+ // now report if there are any package IDs in the second set that aren't
+ // in the first
+ for _, pkgID := range pkgIDsSecond {
+ var p2 *v2_2.Package
+ var okFirst, okSecond bool
+ for _, pkg := range docSecond.Packages {
+ if pkg.PackageSPDXIdentifier == pkgID {
+ okSecond = true
+ p2 = pkg
+ break
+ }
+ }
+ if !okSecond {
+ fmt.Printf("================================\n")
+ fmt.Printf("Package %s has described relationship in second document but ID not found\n", string(pkgID))
+ continue
+ }
+
+ for _, pkg := range docFirst.Packages {
+ if pkg.PackageSPDXIdentifier == pkgID {
+ okFirst = true
+ break
+ }
+ }
+ if !okFirst {
+ fmt.Printf("================================\n")
+ fmt.Printf("Package %s (%s)\n", string(pkgID), p2.PackageName)
+ fmt.Printf(" Found in second document, not found in first\n")
+ }
+ }
+}
diff --git a/examples/7-rdfloader/exampleRDFLoader.go b/examples/7-rdfloader/exampleRDFLoader.go
new file mode 100644
index 0000000..81206a4
--- /dev/null
+++ b/examples/7-rdfloader/exampleRDFLoader.go
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+// Run project: go run exampleRDFLoader.go ../sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/spdx/tools-golang/rdfloader"
+)
+
+func getFilePathFromUser() (string, error) {
+ if len(os.Args) == 1 {
+ // user hasn't specified the rdf file path
+ return "", fmt.Errorf("kindly provide path of the rdf file to be loaded as a spdx-document while running this file")
+ }
+ return os.Args[1], nil
+}
+
+func main() {
+ // example to use the rdfLoader.
+ filePath, ok := getFilePathFromUser()
+ if ok != nil {
+ fmt.Println(fmt.Errorf("%v", ok))
+ os.Exit(1)
+ }
+ file, err := os.Open(filePath)
+ if err != nil {
+ fmt.Println(fmt.Errorf("error opening File: %s", err))
+ os.Exit(1)
+ }
+
+ // loading the spdx-document
+ doc, err := rdfloader.Load2_2(file)
+ if err != nil {
+ fmt.Println(fmt.Errorf("error parsing given spdx document: %s", err))
+ os.Exit(1)
+ }
+
+ // Printing some of the document Information
+ fmt.Println(strings.Repeat("=", 80))
+ fmt.Println("Some Attributes of the Document:")
+ fmt.Printf("Document Name: %s\n", doc.DocumentName)
+ fmt.Printf("DataLicense: %s\n", doc.DataLicense)
+ fmt.Printf("Document Namespace: %s\n", doc.DocumentNamespace)
+ fmt.Printf("SPDX Version: %s\n", doc.SPDXVersion)
+ fmt.Println(strings.Repeat("=", 80))
+}
diff --git a/examples/8-jsontotv/examplejsontotv.go b/examples/8-jsontotv/examplejsontotv.go
new file mode 100644
index 0000000..ee10bd2
--- /dev/null
+++ b/examples/8-jsontotv/examplejsontotv.go
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *json*, *tvsaver*
+
+// This example demonstrates loading an SPDX json from disk into memory,
+// and then re-saving it to a different file on disk in tag-value format .
+// Run project: go run examplejsontotv.go ../sample-docs/json/SPDXJSONExample-v2.2.spdx.json example.spdx
+package main
+
+import (
+ "fmt"
+ "os"
+
+ spdx_json "github.com/spdx/tools-golang/json"
+ "github.com/spdx/tools-golang/tvsaver"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 3 {
+ fmt.Printf("Usage: %v <json-file-in> <spdx-file-out>\n", args[0])
+ fmt.Printf(" Load JSON file <json-file-in>, and\n")
+ fmt.Printf(" save it out to <spdx-file-out>.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a json file, version 2.2
+ doc, err := spdx_json.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", args[1], err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", args[1])
+
+ // we can now save it back to disk, using tvsaver.
+
+ // create a new file for writing
+ fileOut := args[2]
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ // try to save the document to disk as an SPDX tag-value file, version 2.2
+ err = tvsaver.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ // it worked
+ fmt.Printf("Successfully saved %s\n", fileOut)
+}
diff --git a/examples/9-tvtojson/exampletvtojson.go b/examples/9-tvtojson/exampletvtojson.go
new file mode 100644
index 0000000..909677f
--- /dev/null
+++ b/examples/9-tvtojson/exampletvtojson.go
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+// Example for: *tvloader*, *json*
+
+// This example demonstrates loading an SPDX tag-value file from disk into memory,
+// and re-saving it to a different json file on disk.
+// Run project: go run exampletvtojson.go ../sample-docs/tv/hello.spdx example.json
+package main
+
+import (
+ "fmt"
+ "os"
+
+ spdx_json "github.com/spdx/tools-golang/json"
+ "github.com/spdx/tools-golang/tvloader"
+)
+
+func main() {
+
+ // check that we've received the right number of arguments
+ args := os.Args
+ if len(args) != 3 {
+ fmt.Printf("Usage: %v <spdx-file-in> <json-file-out>\n", args[0])
+ fmt.Printf(" Load SPDX 2.2 tag-value file <spdx-file-in>, and\n")
+ fmt.Printf(" save it out to JSON <json-file-out>.\n")
+ return
+ }
+
+ // open the SPDX file
+ fileIn := args[1]
+ r, err := os.Open(fileIn)
+ if err != nil {
+ fmt.Printf("Error while opening %v for reading: %v", fileIn, err)
+ return
+ }
+ defer r.Close()
+
+ // try to load the SPDX file's contents as a tag-value file, version 2.2
+ doc, err := tvloader.Load2_2(r)
+ if err != nil {
+ fmt.Printf("Error while parsing %v: %v", args[1], err)
+ return
+ }
+
+ // if we got here, the file is now loaded into memory.
+ fmt.Printf("Successfully loaded %s\n", args[1])
+
+ // we can now save it back to disk, using jsonsaver.
+
+ // create a new file for writing
+ fileOut := args[2]
+ w, err := os.Create(fileOut)
+ if err != nil {
+ fmt.Printf("Error while opening %v for writing: %v", fileOut, err)
+ return
+ }
+ defer w.Close()
+
+ // try to save the document to disk as JSON file
+ err = spdx_json.Save2_2(doc, w)
+ if err != nil {
+ fmt.Printf("Error while saving %v: %v", fileOut, err)
+ return
+ }
+
+ // it worked
+ fmt.Printf("Successfully saved %s\n", fileOut)
+}
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..2f1ab34
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,120 @@
+SPDX-License-Identifier: CC-BY-4.0
+
+# tools-golang Examples
+
+The `examples/` directory contains examples for how to use the various
+tools-golang sub-packages. Sample commands below should be run from
+within the example's subdirectory.
+
+## 1-load/
+
+*tvloader*, *spdx*
+
+This example demonstrates loading an SPDX tag-value file from disk into memory,
+and printing some of its contents to standard output.
+#### Run project: *go run example_load.go ../sample-docs/tv/hello.spdx*
+
+## 2-load-save/
+
+*tvloader*, *tvsaver*
+
+This example demonstrates loading an SPDX tag-value file from disk into memory,
+and re-saving it to a different file on disk.
+#### Run project: *go run example_load_save.go ../sample-docs/tv/hello.spdx test.spdx*
+
+## 3-build/
+
+*builder*, *tvsaver*
+
+This example demonstrates building an 'empty' SPDX document in memory that
+corresponds to a given directory's contents, including all files with their
+hashes and the package's verification code, and saving the document to disk.
+#### Run project: *go run example_build.go project2 ../../testdata/project2 test.spdx*
+
+## 4-search/
+
+*idsearcher*, *tvsaver*
+
+This example demonstrates building an SPDX document for a directory's contents
+(implicitly using *builder*); searching through that directory for [SPDX
+short-form IDs](https://spdx.org/ids/); filling those IDs into the document's
+Package and File license fields; and saving the resulting document to disk.
+#### Run project: *go run example_search.go project2 ../../testdata/project2/folder test.spdx*
+
+## 5-report/
+
+*reporter*, *tvloader*
+
+This example demonstrates loading an SPDX tag-value file from disk into memory,
+generating a basic report listing counts of the concluded licenses for its
+files, and printing the report to standard output.
+#### Run project: *go run example_report.go ../sample-docs/tv/hello.spdx*
+
+## 6-licensediff
+
+*licensediff*, *tvloader*
+
+This example demonstrates loading two SPDX tag-value files from disk into
+memory, and generating a diff of the concluded licenses for Files in Packages
+with matching IDs in each document.
+
+This is generally only useful when run with two SPDX documents that describe
+licenses for subsequent versions of the same set of files, AND if they have
+the same identifier in both documents.
+#### Run project: *go run example_licensediff.go ../sample-docs/tv/hello.spdx ../sample-docs/tv/hello-modified.spdx*
+
+## 7-rdfloader
+
+*rdfloader*, *spdx*
+
+This example demonstrates loading an SPDX rdf file from disk into memory
+and then printing the corresponding spdx struct for the document.
+#### Run project: *go run exampleRDFLoader.go ../sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf*
+
+## 8-jsontotv
+
+*json*, *tvsaver*
+
+This example demonstrates loading an SPDX json from disk into memory
+and then re-saving it to a different file on disk in tag-value format.
+#### Run project: *go run examplejsontotv.go ../sample-docs/json/SPDXJSONExample-v2.2.spdx.json example.spdx*
+
+## 9-tvtojson
+
+*json*, *tvloader*
+
+This example demonstrates loading an SPDX tag-value from disk into memory
+and then re-saving it to a different file on disk in json format.
+#### Run project: *go run exampletvtojson.go ../sample-docs/tv/hello.spdx example.json*
+
+## 10-jsonloader
+
+*json*
+
+This example demonstrates loading an SPDX json from disk into memory
+and then logging some of the attributes to the console.
+#### Run project: *go run example_json_loader.go ../sample-docs/json/SPDXJSONExample-v2.2.spdx.json*
+
+## 11-yamltotv
+
+*yaml* *tvsaver*
+
+This example demonstrates loading an SPDX yaml from disk into memory
+and then re-saving it to a different file on disk in tag-value format.
+#### Run project: *go run exampleyamltotv.go ../sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml test.spdx*
+
+## 12-tvtoyaml
+
+*yaml* *tvloader*
+
+This example demonstrates loading an SPDX tag-value from disk into memory
+and then re-saving it to a different file on disk in yaml format.
+#### Run project: *go run exampletvtoyaml.go ../sample-docs/tv/hello.spdx example.yaml*
+
+## 13-yamlloader
+
+*yaml*
+
+This example demonstrates loading an SPDX yaml from disk into memory
+and then logging some of the attributes to the console.
+#### Run project: *go run exampleYAMLLoader.go ../sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml*
diff --git a/examples/sample-docs/json/SPDXJSONExample-v2.2.spdx.json b/examples/sample-docs/json/SPDXJSONExample-v2.2.spdx.json
new file mode 100644
index 0000000..89171a1
--- /dev/null
+++ b/examples/sample-docs/json/SPDXJSONExample-v2.2.spdx.json
@@ -0,0 +1,284 @@
+{
+ "SPDXID" : "SPDXRef-DOCUMENT",
+ "spdxVersion" : "SPDX-2.2",
+ "creationInfo" : {
+ "comment" : "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ "created" : "2010-01-29T18:30:22Z",
+ "creators" : [ "Tool: LicenseFind-1.0", "Organization: ExampleCodeInspect ()", "Person: Jane Doe ()" ],
+ "licenseListVersion" : "3.9"
+ },
+ "name" : "SPDX-Tools-v2.0",
+ "dataLicense" : "CC0-1.0",
+ "comment" : "This document was created using SPDX 2.0 using licenses from the web site.",
+ "externalDocumentRefs" : [ {
+ "externalDocumentId" : "DocumentRef-spdx-tool-1.2",
+ "checksum" : {
+ "algorithm" : "SHA1",
+ "checksumValue" : "d6a770ba38583ed4bb4525bd96e50461655d2759"
+ },
+ "spdxDocument" : "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+ } ],
+ "hasExtractedLicensingInfos" : [ {
+ "licenseId" : "LicenseRef-1",
+ "extractedText" : "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/"
+ }, {
+ "licenseId" : "LicenseRef-2",
+ "extractedText" : "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ }, {
+ "licenseId" : "LicenseRef-4",
+ "extractedText" : "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/"
+ }, {
+ "licenseId" : "LicenseRef-Beerware-4.2",
+ "comment" : "The beerware license has a couple of other standard variants.",
+ "extractedText" : "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ "name" : "Beer-Ware License (Version 42)",
+ "seeAlsos" : [ "http://people.freebsd.org/~phk/" ]
+ }, {
+ "licenseId" : "LicenseRef-3",
+ "comment" : "This is tye CyperNeko License",
+ "extractedText" : "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ "name" : "CyberNeko License",
+ "seeAlsos" : [ "http://people.apache.org/~andyc/neko/LICENSE", "http://justasample.url.com" ]
+ } ],
+ "annotations" : [ {
+ "annotationDate" : "2010-01-29T18:30:22Z",
+ "annotationType" : "OTHER",
+ "annotator" : "Person: Jane Doe ()",
+ "comment" : "Document level annotation"
+ }, {
+ "annotationDate" : "2010-02-10T00:00:00Z",
+ "annotationType" : "REVIEW",
+ "annotator" : "Person: Joe Reviewer",
+ "comment" : "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses"
+ }, {
+ "annotationDate" : "2011-03-13T00:00:00Z",
+ "annotationType" : "REVIEW",
+ "annotator" : "Person: Suzanne Reviewer",
+ "comment" : "Another example reviewer."
+ } ],
+ "documentNamespace" : "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ "documentDescribes" : [ "SPDXRef-File", "SPDXRef-Package" ],
+ "packages" : [ {
+ "SPDXID" : "SPDXRef-Package",
+ "annotations" : [ {
+ "annotationDate" : "2011-01-29T18:30:22Z",
+ "annotationType" : "OTHER",
+ "annotator" : "Person: Package Commenter",
+ "comment" : "Package level annotation"
+ } ],
+ "attributionTexts" : [ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually." ],
+ "checksums" : [ {
+ "algorithm" : "MD5",
+ "checksumValue" : "624c1abb3664f4b35547e7c73864ad24"
+ }, {
+ "algorithm" : "SHA1",
+ "checksumValue" : "85ed0817af83a24ad8da68c2b5094de69833983c"
+ }, {
+ "algorithm" : "SHA256",
+ "checksumValue" : "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ } ],
+ "copyrightText" : "Copyright 2008-2010 John Smith",
+ "description" : "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ "downloadLocation" : "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ "externalRefs" : [ {
+ "referenceCategory" : "SECURITY",
+ "referenceLocator" : "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ "referenceType" : "cpe23Type"
+ }, {
+ "comment" : "This is the external ref for Acme",
+ "referenceCategory" : "OTHER",
+ "referenceLocator" : "acmecorp/acmenator/4.1.3-alpha",
+ "referenceType" : "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge"
+ } ],
+ "filesAnalyzed" : true,
+ "hasFiles" : [ "SPDXRef-CommonsLangSrc", "SPDXRef-JenaLib", "SPDXRef-DoapSource" ],
+ "homepage" : "http://ftp.gnu.org/gnu/glibc",
+ "licenseComments" : "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ "licenseConcluded" : "(LGPL-2.0-only OR LicenseRef-3)",
+ "licenseDeclared" : "(LGPL-2.0-only AND LicenseRef-3)",
+ "licenseInfoFromFiles" : [ "GPL-2.0-only", "LicenseRef-2", "LicenseRef-1" ],
+ "name" : "glibc",
+ "originator" : "Organization: ExampleCodeInspect (contact@example.com)",
+ "packageFileName" : "glibc-2.11.1.tar.gz",
+ "packageVerificationCode" : {
+ "packageVerificationCodeExcludedFiles" : [ "./package.spdx" ],
+ "packageVerificationCodeValue" : "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ },
+ "sourceInfo" : "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ "summary" : "GNU C library.",
+ "supplier" : "Person: Jane Doe (jane.doe@example.com)",
+ "versionInfo" : "2.11.1"
+ }, {
+ "SPDXID" : "SPDXRef-fromDoap-1",
+ "copyrightText" : "NOASSERTION",
+ "downloadLocation" : "NOASSERTION",
+ "filesAnalyzed" : false,
+ "homepage" : "http://commons.apache.org/proper/commons-lang/",
+ "licenseConcluded" : "NOASSERTION",
+ "licenseDeclared" : "NOASSERTION",
+ "name" : "Apache Commons Lang"
+ }, {
+ "SPDXID" : "SPDXRef-fromDoap-0",
+ "copyrightText" : "NOASSERTION",
+ "downloadLocation" : "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ "externalRefs" : [ {
+ "referenceCategory" : "PACKAGE_MANAGER",
+ "referenceLocator" : "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ "referenceType" : "purl"
+ } ],
+ "filesAnalyzed" : false,
+ "homepage" : "http://www.openjena.org/",
+ "licenseConcluded" : "NOASSERTION",
+ "licenseDeclared" : "NOASSERTION",
+ "name" : "Jena",
+ "versionInfo" : "3.12.0"
+ }, {
+ "SPDXID" : "SPDXRef-Saxon",
+ "checksums" : [ {
+ "algorithm" : "SHA1",
+ "checksumValue" : "85ed0817af83a24ad8da68c2b5094de69833983c"
+ } ],
+ "copyrightText" : "Copyright Saxonica Ltd",
+ "description" : "The Saxon package is a collection of tools for processing XML documents.",
+ "downloadLocation" : "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ "filesAnalyzed" : false,
+ "homepage" : "http://saxon.sourceforge.net/",
+ "licenseComments" : "Other versions available for a commercial license",
+ "licenseConcluded" : "MPL-1.0",
+ "licenseDeclared" : "MPL-1.0",
+ "name" : "Saxon",
+ "packageFileName" : "saxonB-8.8.zip",
+ "versionInfo" : "8.8"
+ } ],
+ "files" : [ {
+ "SPDXID" : "SPDXRef-DoapSource",
+ "checksums" : [ {
+ "algorithm" : "SHA1",
+ "checksumValue" : "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ } ],
+ "copyrightText" : "Copyright 2010, 2011 Source Auditor Inc.",
+ "fileContributors" : [ "Protecode Inc.", "SPDX Technical Team Members", "Open Logic Inc.", "Source Auditor Inc.", "Black Duck Software In.c" ],
+ "fileName" : "./src/org/spdx/parser/DOAPProject.java",
+ "fileTypes" : [ "SOURCE" ],
+ "licenseConcluded" : "Apache-2.0",
+ "licenseInfoInFiles" : [ "Apache-2.0" ]
+ }, {
+ "SPDXID" : "SPDXRef-CommonsLangSrc",
+ "checksums" : [ {
+ "algorithm" : "SHA1",
+ "checksumValue" : "c2b4e1c67a2d28fced849ee1bb76e7391b93f125"
+ } ],
+ "comment" : "This file is used by Jena",
+ "copyrightText" : "Copyright 2001-2011 The Apache Software Foundation",
+ "fileContributors" : [ "Apache Software Foundation" ],
+ "fileName" : "./lib-source/commons-lang3-3.1-sources.jar",
+ "fileTypes" : [ "ARCHIVE" ],
+ "licenseConcluded" : "Apache-2.0",
+ "licenseInfoInFiles" : [ "Apache-2.0" ],
+ "noticeText" : "Apache Commons Lang\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())"
+ }, {
+ "SPDXID" : "SPDXRef-JenaLib",
+ "checksums" : [ {
+ "algorithm" : "SHA1",
+ "checksumValue" : "3ab4e1c67a2d28fced849ee1bb76e7391b93f125"
+ } ],
+ "comment" : "This file belongs to Jena",
+ "copyrightText" : "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ "fileContributors" : [ "Apache Software Foundation", "Hewlett Packard Inc." ],
+ "fileName" : "./lib-source/jena-2.6.3-sources.jar",
+ "fileTypes" : [ "ARCHIVE" ],
+ "licenseComments" : "This license is used by Jena",
+ "licenseConcluded" : "LicenseRef-1",
+ "licenseInfoInFiles" : [ "LicenseRef-1" ]
+ }, {
+ "SPDXID" : "SPDXRef-File",
+ "annotations" : [ {
+ "annotationDate" : "2011-01-29T18:30:22Z",
+ "annotationType" : "OTHER",
+ "annotator" : "Person: File Commenter",
+ "comment" : "File level annotation"
+ } ],
+ "checksums" : [ {
+ "algorithm" : "SHA1",
+ "checksumValue" : "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ }, {
+ "algorithm" : "MD5",
+ "checksumValue" : "624c1abb3664f4b35547e7c73864ad24"
+ } ],
+ "comment" : "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ "copyrightText" : "Copyright 2008-2010 John Smith",
+ "fileContributors" : [ "The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation" ],
+ "fileName" : "./package/foo.c",
+ "fileTypes" : [ "SOURCE" ],
+ "licenseComments" : "The concluded license was taken from the package level that the file was included in.",
+ "licenseConcluded" : "(LGPL-2.0-only OR LicenseRef-2)",
+ "licenseInfoInFiles" : [ "GPL-2.0-only", "LicenseRef-2" ],
+ "noticeText" : "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
+ } ],
+ "snippets" : [ {
+ "SPDXID" : "SPDXRef-Snippet",
+ "comment" : "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ "copyrightText" : "Copyright 2008-2010 John Smith",
+ "licenseComments" : "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ "licenseConcluded" : "GPL-2.0-only",
+ "licenseInfoInSnippets" : [ "GPL-2.0-only" ],
+ "name" : "from linux kernel",
+ "ranges" : [ {
+ "endPointer" : {
+ "offset" : 420,
+ "reference" : "SPDXRef-DoapSource"
+ },
+ "startPointer" : {
+ "offset" : 310,
+ "reference" : "SPDXRef-DoapSource"
+ }
+ }, {
+ "endPointer" : {
+ "lineNumber" : 23,
+ "reference" : "SPDXRef-DoapSource"
+ },
+ "startPointer" : {
+ "lineNumber" : 5,
+ "reference" : "SPDXRef-DoapSource"
+ }
+ } ],
+ "snippetFromFile" : "SPDXRef-DoapSource"
+ } ],
+ "relationships" : [ {
+ "spdxElementId" : "SPDXRef-DOCUMENT",
+ "relatedSpdxElement" : "SPDXRef-Package",
+ "relationshipType" : "CONTAINS"
+ }, {
+ "spdxElementId" : "SPDXRef-DOCUMENT",
+ "relatedSpdxElement" : "DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement",
+ "relationshipType" : "COPY_OF"
+ }, {
+ "spdxElementId" : "SPDXRef-DOCUMENT",
+ "relatedSpdxElement" : "SPDXRef-File",
+ "relationshipType" : "DESCRIBES"
+ }, {
+ "spdxElementId" : "SPDXRef-DOCUMENT",
+ "relatedSpdxElement" : "SPDXRef-Package",
+ "relationshipType" : "DESCRIBES"
+ }, {
+ "spdxElementId" : "SPDXRef-Package",
+ "relatedSpdxElement" : "SPDXRef-JenaLib",
+ "relationshipType" : "CONTAINS"
+ }, {
+ "spdxElementId" : "SPDXRef-Package",
+ "relatedSpdxElement" : "SPDXRef-Saxon",
+ "relationshipType" : "DYNAMIC_LINK"
+ }, {
+ "spdxElementId" : "SPDXRef-CommonsLangSrc",
+ "relatedSpdxElement" : "NOASSERTION",
+ "relationshipType" : "GENERATED_FROM"
+ }, {
+ "spdxElementId" : "SPDXRef-JenaLib",
+ "relatedSpdxElement" : "SPDXRef-Package",
+ "relationshipType" : "CONTAINS"
+ }, {
+ "spdxElementId" : "SPDXRef-File",
+ "relatedSpdxElement" : "SPDXRef-fromDoap-0",
+ "relationshipType" : "GENERATED_FROM"
+ } ]
+}
diff --git a/examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json b/examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json
new file mode 100644
index 0000000..7ac6549
--- /dev/null
+++ b/examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json
@@ -0,0 +1,406 @@
+{
+ "spdxVersion": "SPDX-2.3",
+ "dataLicense": "CC0-1.0",
+ "SPDXID": "SPDXRef-DOCUMENT",
+ "name": "SPDX-Tools-v2.0",
+ "documentNamespace": "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ "externalDocumentRefs": [
+ {
+ "externalDocumentId": "DocumentRef-spdx-tool-1.2",
+ "spdxDocument": "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ "checksum": {
+ "algorithm": "SHA1",
+ "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2759"
+ }
+ }
+ ],
+ "comment": "This document was created using SPDX 2.0 using licenses from the web site.",
+ "creationInfo": {
+ "licenseListVersion": "3.9",
+ "creators": [
+ "Tool: LicenseFind-1.0",
+ "Organization: ExampleCodeInspect ()",
+ "Person: Jane Doe ()"
+ ],
+ "created": "2010-01-29T18:30:22Z",
+ "comment": "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries."
+ },
+ "packages": [
+ {
+ "name": "glibc",
+ "SPDXID": "SPDXRef-Package",
+ "versionInfo": "2.11.1",
+ "packageFileName": "glibc-2.11.1.tar.gz",
+ "supplier": "Person: Jane Doe (jane.doe@example.com)",
+ "originator": "Organization: ExampleCodeInspect (contact@example.com)",
+ "downloadLocation": "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ "filesAnalyzed": true,
+ "packageVerificationCode": {
+ "packageVerificationCodeValue": "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ "packageVerificationCodeExcludedFiles": [
+ "./package.spdx"
+ ]
+ },
+ "checksums": [
+ {
+ "algorithm": "MD5",
+ "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+ },
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "85ed0817af83a24ad8da68c2b5094de69833983c"
+ },
+ {
+ "algorithm": "SHA256",
+ "checksumValue": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ }
+ ],
+ "homepage": "http://ftp.gnu.org/gnu/glibc",
+ "sourceInfo": "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ "licenseConcluded": "(LGPL-2.0-only OR LicenseRef-3)",
+ "licenseInfoFromFiles": [
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1"
+ ],
+ "licenseDeclared": "(LGPL-2.0-only AND LicenseRef-3)",
+ "licenseComments": "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ "copyrightText": "Copyright 2008-2010 John Smith",
+ "summary": "GNU C library.",
+ "description": "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ "externalRefs": [
+ {
+ "referenceCategory": "SECURITY",
+ "referenceType": "cpe23Type",
+ "referenceLocator": "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ "comment": ""
+ },
+ {
+ "referenceCategory": "OTHER",
+ "referenceType": "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ "referenceLocator": "acmecorp/acmenator/4.1.3-alpha",
+ "comment": "This is the external ref for Acme"
+ }
+ ],
+ "attributionTexts": [
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually."
+ ],
+ "annotations": [
+ {
+ "annotator": "Person: Package Commenter",
+ "annotationDate": "2011-01-29T18:30:22Z",
+ "annotationType": "OTHER",
+ "comment": "Package level annotation"
+ }
+ ]
+ },
+ {
+ "name": "Apache Commons Lang",
+ "SPDXID": "SPDXRef-fromDoap-1",
+ "downloadLocation": "NOASSERTION",
+ "homepage": "http://commons.apache.org/proper/commons-lang/",
+ "licenseDeclared": "NOASSERTION",
+ "copyrightText": "NOASSERTION"
+ },
+ {
+ "name": "Jena",
+ "SPDXID": "SPDXRef-fromDoap-0",
+ "versionInfo": "3.12.0",
+ "downloadLocation": "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ "homepage": "http://www.openjena.org/",
+ "licenseConcluded": "NOASSERTION",
+ "licenseDeclared": "NOASSERTION",
+ "copyrightText": "NOASSERTION",
+ "externalRefs": [
+ {
+ "referenceCategory": "PACKAGE-MANAGER",
+ "referenceType": "purl",
+ "referenceLocator": "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ "comment": ""
+ }
+ ]
+ },
+ {
+ "name": "Saxon",
+ "SPDXID": "SPDXRef-Saxon",
+ "versionInfo": "8.8",
+ "packageFileName": "saxonB-8.8.zip",
+ "downloadLocation": "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "85ed0817af83a24ad8da68c2b5094de69833983c"
+ }
+ ],
+ "homepage": "http://saxon.sourceforge.net/",
+ "licenseConcluded": "MPL-1.0",
+ "licenseDeclared": "MPL-1.0",
+ "licenseComments": "Other versions available for a commercial license",
+ "copyrightText": "Copyright Saxonica Ltd",
+ "description": "The Saxon package is a collection of tools for processing XML documents."
+ },
+ {
+ "name": "centos",
+ "SPDXID": "SPDXRef-CentOS-7",
+ "versionInfo": "centos7.9.2009",
+ "packageFileName": "saxonB-8.8.zip",
+ "downloadLocation": "NOASSERTION",
+ "homepage": "https://www.centos.org/",
+ "copyrightText": "NOASSERTION",
+ "description": "The CentOS container used to run the application.",
+ "primaryPackagePurpose": "CONTAINER",
+ "releaseDate": "2021-10-15T02:38:00Z",
+ "builtDate": "2021-09-15T02:38:00Z",
+ "validUntilDate": "2022-10-15T02:38:00Z"
+ }
+ ],
+ "files": [
+ {
+ "fileName": "./src/org/spdx/parser/DOAPProject.java",
+ "SPDXID": "SPDXRef-DoapSource",
+ "fileTypes": [
+ "SOURCE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ }
+ ],
+ "licenseConcluded": "Apache-2.0",
+ "licenseInfoInFiles": [
+ "Apache-2.0"
+ ],
+ "copyrightText": "Copyright 2010, 2011 Source Auditor Inc.",
+ "fileContributors": [
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c"
+ ]
+ },
+ {
+ "fileName": "./lib-source/commons-lang3-3.1-sources.jar",
+ "SPDXID": "SPDXRef-CommonsLangSrc",
+ "fileTypes": [
+ "ARCHIVE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "c2b4e1c67a2d28fced849ee1bb76e7391b93f125"
+ }
+ ],
+ "licenseConcluded": "Apache-2.0",
+ "licenseInfoInFiles": [
+ "Apache-2.0"
+ ],
+ "copyrightText": "Copyright 2001-2011 The Apache Software Foundation",
+ "comment": "This file is used by Jena",
+ "noticeText": "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ "fileContributors": [
+ "Apache Software Foundation"
+ ]
+ },
+ {
+ "fileName": "./lib-source/jena-2.6.3-sources.jar",
+ "SPDXID": "SPDXRef-JenaLib",
+ "fileTypes": [
+ "ARCHIVE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "3ab4e1c67a2d28fced849ee1bb76e7391b93f125"
+ }
+ ],
+ "licenseConcluded": "LicenseRef-1",
+ "licenseInfoInFiles": [
+ "LicenseRef-1"
+ ],
+ "licenseComments": "This license is used by Jena",
+ "copyrightText": "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ "comment": "This file belongs to Jena",
+ "fileContributors": [
+ "Apache Software Foundation",
+ "Hewlett Packard Inc."
+ ]
+ },
+ {
+ "fileName": "./package/foo.c",
+ "SPDXID": "SPDXRef-File",
+ "fileTypes": [
+ "SOURCE"
+ ],
+ "checksums": [
+ {
+ "algorithm": "SHA1",
+ "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ },
+ {
+ "algorithm": "MD5",
+ "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+ }
+ ],
+ "licenseConcluded": "(LGPL-2.0-only OR LicenseRef-2)",
+ "licenseInfoInFiles": [
+ "GPL-2.0-only",
+ "LicenseRef-2"
+ ],
+ "licenseComments": "The concluded license was taken from the package level that the file was included in.",
+ "copyrightText": "Copyright 2008-2010 John Smith",
+ "comment": "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ "noticeText": "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ "fileContributors": [
+ "The Regents of the University of California",
+ "Modified by Paul Mundt lethal@linux-sh.org",
+ "IBM Corporation"
+ ],
+ "annotations": [
+ {
+ "annotator": "Person: File Commenter",
+ "annotationDate": "2011-01-29T18:30:22Z",
+ "annotationType": "OTHER",
+ "comment": "File level annotation"
+ }
+ ]
+ }
+ ],
+ "hasExtractedLicensingInfos": [
+ {
+ "licenseId": "LicenseRef-1",
+ "extractedText": "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/"
+ },
+ {
+ "licenseId": "LicenseRef-2",
+ "extractedText": "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ },
+ {
+ "licenseId": "LicenseRef-4",
+ "extractedText": "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/"
+ },
+ {
+ "licenseId": "LicenseRef-Beerware-4.2",
+ "extractedText": "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ "name": "Beer-Ware License (Version 42)",
+ "seeAlsos": [
+ "http://people.freebsd.org/~phk/"
+ ],
+ "comment": "The beerware license has a couple of other standard variants."
+ },
+ {
+ "licenseId": "LicenseRef-3",
+ "extractedText": "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ "name": "CyberNeko License",
+ "seeAlsos": [
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com"
+ ],
+ "comment": "This is tye CyperNeko License"
+ }
+ ],
+ "relationships": [
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "SPDXRef-Package",
+ "relationshipType": "CONTAINS"
+ },
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement",
+ "relationshipType": "COPY_OF"
+ },
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "SPDXRef-File",
+ "relationshipType": "DESCRIBES"
+ },
+ {
+ "spdxElementId": "SPDXRef-DOCUMENT",
+ "relatedSpdxElement": "SPDXRef-Package",
+ "relationshipType": "DESCRIBES"
+ },
+ {
+ "spdxElementId": "SPDXRef-Package",
+ "relatedSpdxElement": "SPDXRef-JenaLib",
+ "relationshipType": "CONTAINS"
+ },
+ {
+ "spdxElementId": "SPDXRef-Package",
+ "relatedSpdxElement": "SPDXRef-Saxon",
+ "relationshipType": "DYNAMIC_LINK"
+ },
+ {
+ "spdxElementId": "SPDXRef-CommonsLangSrc",
+ "relatedSpdxElement": "NOASSERTION",
+ "relationshipType": "GENERATED_FROM"
+ },
+ {
+ "spdxElementId": "SPDXRef-JenaLib",
+ "relatedSpdxElement": "SPDXRef-Package",
+ "relationshipType": "CONTAINS"
+ },
+ {
+ "spdxElementId": "SPDXRef-File",
+ "relatedSpdxElement": "SPDXRef-fromDoap-0",
+ "relationshipType": "GENERATED_FROM"
+ }
+ ],
+ "annotations": [
+ {
+ "annotator": "Person: Jane Doe ()",
+ "annotationDate": "2010-01-29T18:30:22Z",
+ "annotationType": "OTHER",
+ "comment": "Document level annotation"
+ },
+ {
+ "annotator": "Person: Joe Reviewer",
+ "annotationDate": "2010-02-10T00:00:00Z",
+ "annotationType": "REVIEW",
+ "comment": "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses"
+ },
+ {
+ "annotator": "Person: Suzanne Reviewer",
+ "annotationDate": "2011-03-13T00:00:00Z",
+ "annotationType": "REVIEW",
+ "comment": "Another example reviewer."
+ }
+ ],
+ "snippets": [
+ {
+ "SPDXID": "SPDXRef-Snippet",
+ "snippetFromFile": "SPDXRef-DoapSource",
+ "ranges": [
+ {
+ "startPointer": {
+ "offset": 310,
+ "reference": "SPDXRef-DoapSource"
+ },
+ "endPointer": {
+ "offset": 420,
+ "reference": "SPDXRef-DoapSource"
+ }
+ },
+ {
+ "startPointer": {
+ "lineNumber": 5,
+ "reference": "SPDXRef-DoapSource"
+ },
+ "endPointer": {
+ "lineNumber": 23,
+ "reference": "SPDXRef-DoapSource"
+ }
+ }
+ ],
+ "licenseConcluded": "GPL-2.0-only",
+ "licenseInfoInSnippets": [
+ "GPL-2.0-only"
+ ],
+ "licenseComments": "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ "copyrightText": "Copyright 2008-2010 John Smith",
+ "comment": "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ "name": "from linux kernel"
+ }
+ ]
+} \ No newline at end of file
diff --git a/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf b/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf
new file mode 100644
index 0000000..df18d36
--- /dev/null
+++ b/examples/sample-docs/rdf/SPDXRdfExample-v2.2.spdx.rdf
@@ -0,0 +1,1365 @@
+<rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#"
+ xmlns:doap="http://usefulinc.com/ns/doap#"
+ xmlns:j.0="http://www.w3.org/2009/pointers#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+ <spdx:Snippet rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Snippet">
+ <spdx:name>from linux kernel</spdx:name>
+ <spdx:copyrightText>Copyright 2008-2010 John Smith</spdx:copyrightText>
+ <spdx:licenseComments>The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.</spdx:licenseComments>
+ <spdx:range>
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource">
+ <spdx:copyrightText>Copyright 2010, 2011 Source Auditor Inc.</spdx:copyrightText>
+ <spdx:fileContributor>Open Logic Inc.</spdx:fileContributor>
+ <spdx:fileName>./src/org/spdx/parser/DOAPProject.java</spdx:fileName>
+ <spdx:fileContributor>Black Duck Software In.c</spdx:fileContributor>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:fileContributor>Source Auditor Inc.</spdx:fileContributor>
+ <spdx:fileContributor>SPDX Technical Team Members</spdx:fileContributor>
+ <spdx:licenseConcluded>
+ <spdx:License rdf:about="http://spdx.org/licenses/Apache-2.0">
+ <rdfs:comment>This license was released January 2004</rdfs:comment>
+ <spdx:name>Apache License 2.0</spdx:name>
+ <spdx:licenseText>Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.</spdx:licenseText>
+ <spdx:standardLicenseHeader>Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+
+you may not use this file except in compliance with the License.
+
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+
+distributed under the License is distributed on an "AS IS" BASIS,
+
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+See the License for the specific language governing permissions and
+
+limitations under the License.</spdx:standardLicenseHeader>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ <spdx:standardLicenseTemplate>&lt;&lt;beginOptional&gt;&gt; Apache License
+
+Version 2.0, January 2004
+
+http://www.apache.org/licenses/&lt;&lt;endOptional&gt;&gt;&lt;&lt;beginOptional&gt;&gt; TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION&lt;&lt;endOptional&gt;&gt;
+
+ &lt;&lt;var;name="bullet";original="1.";match=".{0,20}"&gt;&gt; Definitions.
+
+
+
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+
+
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+
+
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+
+
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+
+
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+
+
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+
+
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+
+
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+
+
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+ &lt;&lt;var;name="bullet";original="2.";match=".{0,20}"&gt;&gt; Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+ &lt;&lt;var;name="bullet";original="3.";match=".{0,20}"&gt;&gt; Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+ &lt;&lt;var;name="bullet";original="4.";match=".{0,20}"&gt;&gt; Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+ &lt;&lt;var;name="bullet";original="(a)";match=".{0,20}"&gt;&gt; You must give any other recipients of the Work or Derivative Works a copy of this License; and
+
+ &lt;&lt;var;name="bullet";original="(b)";match=".{0,20}"&gt;&gt; You must cause any modified files to carry prominent notices stating that You changed the files; and
+
+ &lt;&lt;var;name="bullet";original="(c)";match=".{0,20}"&gt;&gt; You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+
+ &lt;&lt;var;name="bullet";original="(d)";match=".{0,20}"&gt;&gt; If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+ &lt;&lt;var;name="bullet";original="5.";match=".{0,20}"&gt;&gt; Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+ &lt;&lt;var;name="bullet";original="6.";match=".{0,20}"&gt;&gt; Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+ &lt;&lt;var;name="bullet";original="7.";match=".{0,20}"&gt;&gt; Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+ &lt;&lt;var;name="bullet";original="8.";match=".{0,20}"&gt;&gt; Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+ &lt;&lt;var;name="bullet";original="9.";match=".{0,20}"&gt;&gt; Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.&lt;&lt;beginOptional&gt;&gt; END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
+
+Copyright &lt;&lt;var;name="copyright";original="[yyyy] [name of copyright owner]";match=".+"&gt;&gt;
+
+Licensed under the Apache License, Version 2.0 (the "License");
+
+you may not use this file except in compliance with the License.
+
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+
+distributed under the License is distributed on an "AS IS" BASIS,
+
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+See the License for the specific language governing permissions and
+
+limitations under the License.&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseTemplate>
+ <rdfs:seeAlso>https://opensource.org/licenses/Apache-2.0</rdfs:seeAlso>
+ <rdfs:seeAlso>http://www.apache.org/licenses/LICENSE-2.0</rdfs:seeAlso>
+ <spdx:standardLicenseHeaderTemplate>Copyright &lt;&lt;var;name="copyright";original="[yyyy] [name of copyright owner]";match=".+"&gt;&gt;
+
+Licensed under the Apache License, Version 2.0 (the "License");
+
+you may not use this file except in compliance with the License.
+
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+
+distributed under the License is distributed on an "AS IS" BASIS,
+
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+See the License for the specific language governing permissions and
+
+limitations under the License.</spdx:standardLicenseHeaderTemplate>
+ <spdx:licenseId>Apache-2.0</spdx:licenseId>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ </spdx:License>
+ </spdx:licenseConcluded>
+ <spdx:fileContributor>Protecode Inc.</spdx:fileContributor>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/Apache-2.0"/>
+ <spdx:fileDependency>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-JenaLib">
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ <spdx:licenseConcluded>
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-1">
+ <spdx:extractedText>/*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</spdx:extractedText> <spdx:licenseId>LicenseRef-1</spdx:licenseId>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:licenseConcluded>
+ <spdx:licenseComments>This license is used by Jena</spdx:licenseComments>
+ <spdx:fileContributor>Hewlett Packard Inc.</spdx:fileContributor>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_archive"/>
+ <spdx:copyrightText>(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP</spdx:copyrightText>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>3ab4e1c67a2d28fced849ee1bb76e7391b93f125</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:fileContributor>Apache Software Foundation</spdx:fileContributor>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-1"/>
+ <spdx:fileDependency>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-CommonsLangSrc">
+ <spdx:noticeText>Apache Commons Lang
+Copyright 2001-2011 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software from the Spring Framework,
+under the Apache License 2.0 (see: StringUtils.containsWhitespace())</spdx:noticeText>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/Apache-2.0"/>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/Apache-2.0"/>
+ <spdx:fileContributor>Apache Software Foundation</spdx:fileContributor>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>c2b4e1c67a2d28fced849ee1bb76e7391b93f125</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:fileName>./lib-source/commons-lang3-3.1-sources.jar</spdx:fileName>
+ <spdx:copyrightText>Copyright 2001-2011 The Apache Software Foundation</spdx:copyrightText>
+ <rdfs:comment>This file is used by Jena</rdfs:comment>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://commons.apache.org/proper/commons-lang/</doap:homepage>
+ <doap:name>Apache Commons Lang</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_archive"/>
+ </spdx:File>
+ </spdx:fileDependency>
+ <rdfs:comment>This file belongs to Jena</rdfs:comment>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_dynamicLink"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Package">
+ <spdx:licenseConcluded>
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/LGPL-2.0-only">
+ <spdx:licenseId>LGPL-2.0-only</spdx:licenseId>
+ <rdfs:seeAlso>https://www.gnu.org/licenses/old-licenses/lgpl-2.0-standalone.html</rdfs:seeAlso>
+ <spdx:standardLicenseTemplate
+ >&lt;&lt;beginOptional&gt;&gt; GNU LIBRARY GENERAL PUBLIC LICENSE
+
+Version 2, June 1991&lt;&lt;endOptional&gt;&gt; &lt;&lt;var;name="copyright";original="Copyright (C) 1991 Free Software Foundation, Inc.
+
+51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA";match=".{0,1000}"&gt;&gt;
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
+
+This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it.
+
+For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
+
+Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library.
+
+Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+
+Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license.
+
+The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such.
+
+Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better.
+
+However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries.
+
+The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library.
+
+Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ &lt;&lt;var;name="bullet";original="0.";match=".{0,20}"&gt;&gt; This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
+
+ Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
+
+ &lt;&lt;var;name="bullet";original="1.";match=".{0,20}"&gt;&gt; You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
+
+ You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+
+ &lt;&lt;var;name="bullet";original="2.";match=".{0,20}"&gt;&gt; You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; The modified work must itself be a software library.
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
+
+ &lt;&lt;var;name="bullet";original="c)";match=".{0,20}"&gt;&gt; You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
+
+ &lt;&lt;var;name="bullet";original="d)";match=".{0,20}"&gt;&gt; If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
+
+ These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+
+ Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
+
+ In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+ &lt;&lt;var;name="bullet";original="3.";match=".{0,20}"&gt;&gt; You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
+
+ Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
+
+ &lt;&lt;var;name="bullet";original="4.";match=".{0,20}"&gt;&gt; You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
+
+ &lt;&lt;var;name="bullet";original="5.";match=".{0,20}"&gt;&gt; A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
+
+ &lt;&lt;var;name="bullet";original="6.";match=".{0,20}"&gt;&gt; As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
+
+ &lt;&lt;var;name="bullet";original="c)";match=".{0,20}"&gt;&gt; If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
+
+ &lt;&lt;var;name="bullet";original="d)";match=".{0,20}"&gt;&gt; Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+
+ It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
+
+ &lt;&lt;var;name="bullet";original="7.";match=".{0,20}"&gt;&gt; You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
+
+ &lt;&lt;var;name="bullet";original="8.";match=".{0,20}"&gt;&gt; You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+
+ &lt;&lt;var;name="bullet";original="9.";match=".{0,20}"&gt;&gt; You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
+
+ &lt;&lt;var;name="bullet";original="10.";match=".{0,20}"&gt;&gt; Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
+
+ &lt;&lt;var;name="bullet";original="11.";match=".{0,20}"&gt;&gt; If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
+
+ If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
+
+ It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+
+ &lt;&lt;var;name="bullet";original="12.";match=".{0,20}"&gt;&gt; If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+
+ &lt;&lt;var;name="bullet";original="13.";match=".{0,20}"&gt;&gt; The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
+
+ &lt;&lt;var;name="bullet";original="14.";match=".{0,20}"&gt;&gt; If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ &lt;&lt;var;name="bullet";original="15.";match=".{0,20}"&gt;&gt; BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ &lt;&lt;var;name="bullet";original="16.";match=".{0,20}"&gt;&gt; IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.&lt;&lt;beginOptional&gt;&gt; END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Libraries
+
+If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License).
+
+To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+one line to give the library's name and an idea of what it does.
+
+Copyright (C) year name of author
+
+This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
+
+You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names:
+
+Yoyodyne, Inc., hereby disclaims all copyright interest in
+
+the library `Frob' (a library for tweaking knobs) written
+
+by James Random Hacker.
+
+signature of Ty Coon, 1 April 1990
+
+Ty Coon, President of Vice
+
+That's all there is to it!&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseTemplate>
+ <spdx:standardLicenseHeaderTemplate
+ >Copyright (C) &lt;&lt;var;name="copyright";original="year name of author";match=".+"&gt;&gt;
+
+This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; version 2.
+
+This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
+
+You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.</spdx:standardLicenseHeaderTemplate>
+ <spdx:standardLicenseHeader
+ >Copyright (C) year name of author
+
+This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; version 2.
+
+This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
+
+You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.</spdx:standardLicenseHeader>
+ <spdx:licenseText>GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the library's name and an idea of what it does. Copyright (C) year name of author This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. signature of Ty Coon, 1 April 1990 Ty Coon, President of Vice That's all there is to it!</spdx:licenseText>
+ <rdfs:comment>This license was released: June 1991. This license has been superseded by LGPL-2.1. This license identifier refers to the choice to use the code under LGPL-2.0-only, as distinguished from use of code under LGPL-2.0-or-later (i.e., LGPL-2.0 or some later version). The license notice (as seen in the Standard License Header field below) states which of these applies to the code in the file. The example in the exhibit to the license shows the license notice for the "or later" approach.</rdfs:comment>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <spdx:name>GNU Library General Public License v2 only</spdx:name>
+ </spdx:License>
+ </spdx:member>
+ <spdx:member>
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-3">
+ <rdfs:comment>This is tye CyperNeko License</rdfs:comment>
+ <rdfs:seeAlso>http://justasample.url.com</rdfs:seeAlso>
+ <rdfs:seeAlso>http://people.apache.org/~andyc/neko/LICENSE</rdfs:seeAlso>
+ <spdx:name>CyberNeko License</spdx:name>
+ <spdx:extractedText>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.</spdx:extractedText>
+ <spdx:licenseId>LicenseRef-3</spdx:licenseId>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:member>
+ </spdx:DisjunctiveLicenseSet>
+ </spdx:licenseConcluded>
+ <spdx:downloadLocation>http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz</spdx:downloadLocation>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:checksum>
+ <spdx:Checksum rdf:nodeID="A0">
+ <spdx:checksumValue>624c1abb3664f4b35547e7c73864ad24</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:versionInfo>2.11.1</spdx:versionInfo>
+ <spdx:licenseInfoFromFiles>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-only">
+ <spdx:standardLicenseHeader>Copyright (C) yyyy name of author
+
+This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</spdx:standardLicenseHeader>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <rdfs:seeAlso>https://opensource.org/licenses/GPL-2.0</rdfs:seeAlso>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ <rdfs:seeAlso>https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html</rdfs:seeAlso>
+ <rdfs:comment>This license was released: June 1991. This license identifier refers to the choice to use the code under GPL-2.0-only, as distinguished from use of code under GPL-2.0-or-later (i.e., GPL-2.0 or some later version). The license notice (as seen in the Standard License Header field below) states which of these applies to the code in the file. The example in the exhibit to the license shows the license notice for the "or later" approach.</rdfs:comment>
+ <spdx:licenseId>GPL-2.0-only</spdx:licenseId>
+ <spdx:standardLicenseTemplate
+ >&lt;&lt;beginOptional&gt;&gt; GNU GENERAL PUBLIC LICENSE
+
+Version 2, June 1991&lt;&lt;endOptional&gt;&gt;
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. &lt;&lt;var;name="incComma";original="";match=",|"&gt;&gt;
+
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301&lt;&lt;beginOptional&gt;&gt;, &lt;&lt;endOptional&gt;&gt; USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+&lt;&lt;var;name="termsTitle";original="";match="GNU GENERAL PUBLIC LICENSE|"&gt;&gt; TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ &lt;&lt;var;name="bullet";original="0.";match=".{0,20}"&gt;&gt; This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
+
+ Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
+
+ &lt;&lt;var;name="bullet";original="1.";match=".{0,20}"&gt;&gt; You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
+
+ You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+
+ &lt;&lt;var;name="bullet";original="2.";match=".{0,20}"&gt;&gt; You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
+
+ &lt;&lt;var;name="bullet";original="c)";match=".{0,20}"&gt;&gt; If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
+
+ These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+
+ Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
+
+ In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+ &lt;&lt;var;name="bullet";original="3.";match=".{0,20}"&gt;&gt; You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ &lt;&lt;var;name="bullet";original="c)";match=".{0,20}"&gt;&gt; Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
+
+ The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+
+ If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
+
+ &lt;&lt;var;name="bullet";original="4.";match=".{0,20}"&gt;&gt; You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+
+ &lt;&lt;var;name="bullet";original="5.";match=".{0,20}"&gt;&gt; You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
+
+ &lt;&lt;var;name="bullet";original="6.";match=".{0,20}"&gt;&gt; Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
+
+ &lt;&lt;var;name="bullet";original="7.";match=".{0,20}"&gt;&gt; If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
+
+ If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
+
+ It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+
+ &lt;&lt;var;name="bullet";original="8.";match=".{0,20}"&gt;&gt; If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+
+ &lt;&lt;var;name="bullet";original="9.";match=".{0,20}"&gt;&gt; The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
+
+ &lt;&lt;var;name="bullet";original="10.";match=".{0,20}"&gt;&gt; If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ &lt;&lt;var;name="bullet";original="11.";match=".{0,20}"&gt;&gt; BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ &lt;&lt;var;name="bullet";original="12.";match=".{0,20}"&gt;&gt; IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.&lt;&lt;beginOptional&gt;&gt; END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+&lt;&lt;beginOptional&gt;&gt;&lt;&lt;&lt;endOptional&gt;&gt;one line to give the program's name and &lt;&lt;var;name="ideaArticle";original="an";match="a brief|an"&gt;&gt; idea of what it does.&lt;&lt;beginOptional&gt;&gt;&gt;&lt;&lt;endOptional&gt;&gt;
+
+Copyright (C)&lt;&lt;beginOptional&gt;&gt;&lt;&lt;&lt;endOptional&gt;&gt; &lt;&lt;var;name="templateYear";original="yyyy";match="yyyy|year"&gt;&gt;&lt;&lt;beginOptional&gt;&gt;&gt; &lt;&lt;endOptional&gt;&gt;&lt;&lt;beginOptional&gt;&gt; &lt;&lt;&lt;endOptional&gt;&gt;name of author&lt;&lt;beginOptional&gt;&gt;&gt;&lt;&lt;endOptional&gt;&gt;
+
+This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301&lt;&lt;beginOptional&gt;&gt;, &lt;&lt;endOptional&gt;&gt; USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
+
+Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
+
+Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+&lt;&lt;beginOptional&gt;&gt;&lt;&lt;&lt;endOptional&gt;&gt;signature of Ty Coon&lt;&lt;beginOptional&gt;&gt; &gt;&lt;&lt;endOptional&gt;&gt;, 1 April 1989 Ty Coon, President of Vice&lt;&lt;endOptional&gt;&gt;&lt;&lt;beginOptional&gt;&gt; This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseTemplate>
+ <spdx:licenseText>GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C)&lt; yyyy&gt; This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.</spdx:licenseText>
+ <spdx:name>GNU General Public License v2.0 only</spdx:name>
+ <spdx:standardLicenseHeaderTemplate
+ >Copyright (C) &lt;&lt;var;name="copyright";original="yyyy name of author";match=".+"&gt;&gt;
+
+This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301&lt;&lt;beginOptional&gt;&gt;, &lt;&lt;endOptional&gt;&gt; USA.</spdx:standardLicenseHeaderTemplate>
+ </spdx:License>
+ </spdx:licenseInfoFromFiles>
+ <spdx:licenseComments>The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.</spdx:licenseComments>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_contains"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-JenaLib"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <rdfs:comment>This is the external ref for Acme</rdfs:comment>
+ <spdx:referenceLocator>acmecorp/acmenator/4.1.3-alpha</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_other"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ <spdx:packageVerificationCode>
+ <spdx:PackageVerificationCode>
+ <spdx:packageVerificationCodeValue
+ >d6a770ba38583ed4bb4525bd96e50461655d2758</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile
+ >excludes: ./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx:PackageVerificationCode>
+ </spdx:packageVerificationCode>
+ <spdx:licenseDeclared>
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0-only"/>
+ <spdx:member rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-3"/>
+ </spdx:ConjunctiveLicenseSet>
+ </spdx:licenseDeclared>
+ <spdx:licenseInfoFromFiles rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-1"/>
+ <doap:homepage>http://ftp.gnu.org/gnu/glibc</doap:homepage>
+ <spdx:name>glibc</spdx:name>
+ <spdx:supplier>Person: Jane Doe (jane.doe@example.com)</spdx:supplier>
+ <spdx:originator>Organization: ExampleCodeInspect (contact@example.com)</spdx:originator>
+ <spdx:hasFile rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <spdx:licenseInfoFromFiles>
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-2">
+ <spdx:extractedText>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.</spdx:extractedText>
+ <spdx:licenseId>LicenseRef-2</spdx:licenseId>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:licenseInfoFromFiles>
+ <spdx:hasFile rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-JenaLib"/>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Package level annotation</rdfs:comment>
+ <spdx:annotator>Person: Package Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ <spdx:hasFile rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-CommonsLangSrc"/>
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ <spdx:checksum>
+ <spdx:Checksum rdf:nodeID="A1">
+ <spdx:checksumValue>85ed0817af83a24ad8da68c2b5094de69833983c</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:sourceInfo>uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.</spdx:sourceInfo>
+ <spdx:packageFileName>glibc-2.11.1.tar.gz</spdx:packageFileName>
+ <spdx:summary>GNU C library.</spdx:summary>
+ <spdx:filesAnalyzed>true</spdx:filesAnalyzed>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_dynamicLink"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon">
+ <spdx:name>Saxon</spdx:name>
+ <spdx:description>The Saxon package is a collection of tools for processing XML documents.</spdx:description>
+ <doap:homepage>http://saxon.sourceforge.net/</doap:homepage>
+ <spdx:checksum rdf:nodeID="A1"/>
+ <spdx:versionInfo>8.8</spdx:versionInfo>
+ <spdx:licenseComments>Other versions available for a commercial license</spdx:licenseComments>
+ <spdx:downloadLocation>https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download</spdx:downloadLocation>
+ <spdx:packageFileName>saxonB-8.8.zip</spdx:packageFileName>
+ <spdx:filesAnalyzed>false</spdx:filesAnalyzed>
+ <spdx:licenseConcluded>
+ <spdx:License rdf:about="http://spdx.org/licenses/MPL-1.0">
+ <rdfs:seeAlso>https://opensource.org/licenses/MPL-1.0</rdfs:seeAlso>
+ <rdfs:seeAlso>http://www.mozilla.org/MPL/MPL-1.0.html</rdfs:seeAlso>
+ <rdfs:comment>This license has been superseded by v1.1</rdfs:comment>
+ <spdx:licenseText>MOZILLA PUBLIC LICENSE Version 1.0 1. Definitions. 1.1. "Contributor" means each entity that creates or contributes to the creation of Modifications. 1.2. "Contributor Version" means the combination of the Original Code, prior Modifications used by a Contributor, and the Modifications made by that particular Contributor. 1.3. "Covered Code" means the Original Code or Modifications or the combination of the Original Code and Modifications, in each case including portions thereof. 1.4. "Electronic Distribution Mechanism" means a mechanism generally accepted in the software development community for the electronic transfer of data. 1.5. "Executable" means Covered Code in any form other than Source Code. 1.6. "Initial Developer" means the individual or entity identified as the Initial Developer in the Source Code notice required by Exhibit A. 1.7. "Larger Work" means a work which combines Covered Code or portions thereof with code not governed by the terms of this License. 1.8. "License" means this document. 1.9. "Modifications" means any addition to or deletion from the substance or structure of either the Original Code or any previous Modifications. When Covered Code is released as a series of files, a Modification is: A. Any addition to or deletion from the contents of a file containing Original Code or previous Modifications. B. Any new file that contains any part of the Original Code or previous Modifications. 1.10. "Original Code" means Source Code of computer software code which is described in the Source Code notice required by Exhibit A as Original Code, and which, at the time of its release under this License is not already Covered Code governed by this License. 1.11. "Source Code" means the preferred form of the Covered Code for making modifications to it, including all modules it contains, plus any associated interface definition files, scripts used to control compilation and installation of an Executable, or a list of source code differential comparisons against either the Original Code or another well known, available Covered Code of the Contributor's choice. The Source Code can be in a compressed or archival form, provided the appropriate decompression or de-archiving software is widely available for no charge. 1.12. "You" means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 6.1. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of fifty percent (50%) or more of the outstanding shares or beneficial ownership of such entity. 2. Source Code License. 2.1. The Initial Developer Grant. The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: (a) to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, or as part of a Larger Work; and (b) under patents now or hereafter owned or controlled by Initial Developer, to make, have made, use and sell ("Utilize") the Original Code (or portions thereof), but solely to the extent that any such patent is reasonably necessary to enable You to Utilize the Original Code (or portions thereof) and not to any greater extent that may be necessary to Utilize further Modifications or combinations. 2.2. Contributor Grant. Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: (a) to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof) either on an unmodified basis, with other Modifications, as Covered Code or as part of a Larger Work; and (b) under patents now or hereafter owned or controlled by Contributor, to Utilize the Contributor Version (or portions thereof), but solely to the extent that any such patent is reasonably necessary to enable You to Utilize the Contributor Version (or portions thereof), and not to any greater extent that may be necessary to Utilize further Modifications or combinations. 3. Distribution Obligations. 3.1. Application of License. The Modifications which You create or to which You contribute are governed by the terms of this License, including without limitation Section 2.2. The Source Code version of Covered Code may be distributed only under the terms of this License or a future version of this License released under Section 6.1, and You must include a copy of this License with every copy of the Source Code You distribute. You may not offer or impose any terms on any Source Code version that alters or restricts the applicable version of this License or the recipients' rights hereunder. However, You may include an additional document offering the additional rights described in Section 3.5. 3.2. Availability of Source Code. Any Modification which You create or to which You contribute must be made available in Source Code form under the terms of this License either on the same media as an Executable version or via an accepted Electronic Distribution Mechanism to anyone to whom you made an Executable version available; and if made available via Electronic Distribution Mechanism, must remain available for at least twelve (12) months after the date it initially became available, or at least six (6) months after a subsequent version of that particular Modification has been made available to such recipients. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party. 3.3. Description of Modifications. You must cause all Covered Code to which you contribute to contain a file documenting the changes You made to create that Covered Code and the date of any change. You must include a prominent statement that the Modification is derived, directly or indirectly, from Original Code provided by the Initial Developer and including the name of the Initial Developer in (a) the Source Code, and (b) in any notice in an Executable version or related documentation in which You describe the origin or ownership of the Covered Code. 3.4. Intellectual Property Matters (a) Third Party Claims. If You have knowledge that a party claims an intellectual property right in particular functionality or code (or its utilization under this License), you must include a text file with the source code distribution titled "LEGAL" which describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If you obtain such knowledge after You make Your Modification available as described in Section 3.2, You shall promptly modify the LEGAL file in all copies You make available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Covered Code that new knowledge has been obtained. (b) Contributor APIs. If Your Modification is an application programming interface and You own or control patents which are reasonably necessary to implement that API, you must also include this information in the LEGAL file. 3.5. Required Notices. You must duplicate the notice in Exhibit A in each file of the Source Code, and this License in any documentation for the Source Code, where You describe recipients' rights relating to Covered Code. If You created one or more Modification(s), You may add your name as a Contributor to the notice described in Exhibit A. If it is not possible to put such notice in a particular Source Code file due to its structure, then you must include such notice in a location (such as a relevant directory file) where a user would be likely to look for such a notice. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Code. However, You may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear than any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 3.6. Distribution of Executable Versions. You may distribute Covered Code in Executable form only if the requirements of Section 3.1-3.5 have been met for that Covered Code, and if You include a notice stating that the Source Code version of the Covered Code is available under the terms of this License, including a description of how and where You have fulfilled the obligations of Section 3.2. The notice must be conspicuously included in any notice in an Executable version, related documentation or collateral in which You describe recipients' rights relating to the Covered Code. You may distribute the Executable version of Covered Code under a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable version does not attempt to limit or alter the recipient's rights in the Source Code version from the rights set forth in this License. If You distribute the Executable version under a different license You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or any Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer. 3.7. Larger Works. You may create a Larger Work by combining Covered Code with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Code. 4. Inability to Comply Due to Statute or Regulation. If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Code due to statute or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 3.4 and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Application of this License. This License applies to code to which the Initial Developer has attached the notice in Exhibit A, and to related Covered Code. 6. Versions of the License. 6.1. New Versions. Netscape Communications Corporation ("Netscape") may publish revised and/or new versions of the License from time to time. Each version will be given a distinguishing version number. 6.2. Effect of New Versions. Once Covered Code has been published under a particular version of the License, You may always continue to use it under the terms of that version. You may also choose to use such Covered Code under the terms of any subsequent version of the License published by Netscape. No one other than Netscape has the right to modify the terms applicable to Covered Code created under this License. 6.3. Derivative Works. If you create or use a modified version of this License (which you may only do in order to apply it to code which is not already Covered Code governed by this License), you must (a) rename Your license so that the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", "NPL" or any confusingly similar phrase do not appear anywhere in your license and (b) otherwise make it clear that your version of the license contains terms which differ from the Mozilla Public License and Netscape Public License. (Filling in the name of the Initial Developer, Original Code or Contributor in the notice described in Exhibit A shall not of themselves be deemed to be modifications of this License.) 7. DISCLAIMER OF WARRANTY. COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 8. TERMINATION. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. All sublicenses to the Covered Code which are properly granted shall survive any termination of this License. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 9. LIMITATION OF LIABILITY. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO YOU OR ANY OTHER PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 10. U.S. GOVERNMENT END USERS. The Covered Code is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" and "commercial computer software documentation," as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Code with only those rights set forth herein. 11. MISCELLANEOUS. This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. With respect to disputes in which at least one party is a citizen of, or an entity chartered or registered to do business in, the United States of America: (a) unless otherwise agreed in writing, all disputes relating to this License (excepting any dispute relating to intellectual property rights) shall be subject to final and binding arbitration, with the losing party paying all costs of arbitration; (b) any arbitration relating to this Agreement shall be held in Santa Clara County, California, under the auspices of JAMS/EndDispute; and (c) any litigation relating to this Agreement shall be subject to the jurisdiction of the Federal Courts of the Northern District of California, with venue lying in Santa Clara County, California, with the losing party responsible for costs, including without limitation, court costs and reasonable attorneys fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. 12. RESPONSIBILITY FOR CLAIMS. Except in cases where another Contributor has failed to comply with Section 3.4, You are responsible for damages arising, directly or indirectly, out of Your utilization of rights under this License, based on the number of copies of Covered Code you made available, the revenues you received from utilizing such rights, and other relevant factors. You agree to work with affected parties to distribute responsibility on an equitable basis. EXHIBIT A. "The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is _____ . The Initial Developer of the Original Code is _____ . Portions created by _____ are Copyright (C) _____ . All Rights Reserved. Contributor(s): _____ ."</spdx:licenseText>
+ <spdx:name>Mozilla Public License 1.0</spdx:name>
+ <spdx:standardLicenseHeaderTemplate
+ >&lt;&lt;beginOptional&gt;&gt;"&lt;&lt;endOptional&gt;&gt;The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
+
+The Original Code is &lt;&lt;var;name="code";original="_____";match=".+"&gt;&gt; . The Initial Developer of the Original Code is &lt;&lt;var;name="InitialDeveloper";original="_____";match=".+"&gt;&gt; . Portions created by &lt;&lt;var;name="createdby";original="_____";match=".+"&gt;&gt; are Copyright (C) &lt;&lt;var;name="copyright";original="_____";match=".+"&gt;&gt; . All Rights Reserved. Contributor(s): &lt;&lt;var;name="contributor";original="_____";match=".+"&gt;&gt; .&lt;&lt;beginOptional&gt;&gt;"&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseHeaderTemplate>
+ <spdx:standardLicenseTemplate
+ >&lt;&lt;beginOptional&gt;&gt; MOZILLA PUBLIC LICENSE
+
+Version 1.0&lt;&lt;endOptional&gt;&gt;
+
+ &lt;&lt;var;name="bullet";original="1.";match=".{0,20}"&gt;&gt; Definitions.
+
+ &lt;&lt;var;name="bullet";original="1.1.";match=".{0,20}"&gt;&gt; "Contributor" means each entity that creates or contributes to the creation of Modifications.
+
+ &lt;&lt;var;name="bullet";original="1.2.";match=".{0,20}"&gt;&gt; "Contributor Version" means the combination of the Original Code, prior Modifications used by a Contributor, and the Modifications made by that particular Contributor.
+
+ &lt;&lt;var;name="bullet";original="1.3.";match=".{0,20}"&gt;&gt; "Covered Code" means the Original Code or Modifications or the combination of the Original Code and Modifications, in each case including portions thereof.
+
+ &lt;&lt;var;name="bullet";original="1.4.";match=".{0,20}"&gt;&gt; "Electronic Distribution Mechanism" means a mechanism generally accepted in the software development community for the electronic transfer of data.
+
+ &lt;&lt;var;name="bullet";original="1.5.";match=".{0,20}"&gt;&gt; "Executable" means Covered Code in any form other than Source Code.
+
+ &lt;&lt;var;name="bullet";original="1.6.";match=".{0,20}"&gt;&gt; "Initial Developer" means the individual or entity identified as the Initial Developer in the Source Code notice required by Exhibit A.
+
+ &lt;&lt;var;name="bullet";original="1.7.";match=".{0,20}"&gt;&gt; "Larger Work" means a work which combines Covered Code or portions thereof with code not governed by the terms of this License.
+
+ &lt;&lt;var;name="bullet";original="1.8.";match=".{0,20}"&gt;&gt; "License" means this document.
+
+ &lt;&lt;var;name="bullet";original="1.9.";match=".{0,20}"&gt;&gt; "Modifications" means any addition to or deletion from the substance or structure of either the Original Code or any previous Modifications. When Covered Code is released as a series of files, a Modification is:
+
+ &lt;&lt;var;name="bullet";original="A.";match=".{0,20}"&gt;&gt; Any addition to or deletion from the contents of a file containing Original Code or previous Modifications.
+
+ &lt;&lt;var;name="bullet";original="B.";match=".{0,20}"&gt;&gt; Any new file that contains any part of the Original Code or previous Modifications.
+
+ &lt;&lt;var;name="bullet";original="1.10.";match=".{0,20}"&gt;&gt; "Original Code" means Source Code of computer software code which is described in the Source Code notice required by Exhibit A as Original Code, and which, at the time of its release under this License is not already Covered Code governed by this License.
+
+ &lt;&lt;var;name="bullet";original="1.11.";match=".{0,20}"&gt;&gt; "Source Code" means the preferred form of the Covered Code for making modifications to it, including all modules it contains, plus any associated interface definition files, scripts used to control compilation and installation of an Executable, or a list of source code differential comparisons against either the Original Code or another well known, available Covered Code of the Contributor's choice. The Source Code can be in a compressed or archival form, provided the appropriate decompression or de-archiving software is widely available for no charge.
+
+ &lt;&lt;var;name="bullet";original="1.12.";match=".{0,20}"&gt;&gt; "You" means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 6.1. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of fifty percent (50%) or more of the outstanding shares or beneficial ownership of such entity.
+
+ &lt;&lt;var;name="bullet";original="2.";match=".{0,20}"&gt;&gt; Source Code License.
+
+ &lt;&lt;var;name="bullet";original="2.1.";match=".{0,20}"&gt;&gt; The Initial Developer Grant.
+
+ The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
+
+ &lt;&lt;var;name="bullet";original="(a)";match=".{0,20}"&gt;&gt; to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, or as part of a Larger Work; and
+
+ &lt;&lt;var;name="bullet";original="(b)";match=".{0,20}"&gt;&gt; under patents now or hereafter owned or controlled by Initial Developer, to make, have made, use and sell ("Utilize") the Original Code (or portions thereof), but solely to the extent that any such patent is reasonably necessary to enable You to Utilize the Original Code (or portions thereof) and not to any greater extent that may be necessary to Utilize further Modifications or combinations.
+
+ &lt;&lt;var;name="bullet";original="2.2.";match=".{0,20}"&gt;&gt; Contributor Grant.
+
+ Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
+
+ &lt;&lt;var;name="bullet";original="(a)";match=".{0,20}"&gt;&gt; to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof) either on an unmodified basis, with other Modifications, as Covered Code or as part of a Larger Work; and
+
+ &lt;&lt;var;name="bullet";original="(b)";match=".{0,20}"&gt;&gt; under patents now or hereafter owned or controlled by Contributor, to Utilize the Contributor Version (or portions thereof), but solely to the extent that any such patent is reasonably necessary to enable You to Utilize the Contributor Version (or portions thereof), and not to any greater extent that may be necessary to Utilize further Modifications or combinations.
+
+ &lt;&lt;var;name="bullet";original="3.";match=".{0,20}"&gt;&gt; Distribution Obligations.
+
+ &lt;&lt;var;name="bullet";original="3.1.";match=".{0,20}"&gt;&gt; Application of License.
+
+ The Modifications which You create or to which You contribute are governed by the terms of this License, including without limitation Section 2.2. The Source Code version of Covered Code may be distributed only under the terms of this License or a future version of this License released under Section 6.1, and You must include a copy of this License with every copy of the Source Code You distribute. You may not offer or impose any terms on any Source Code version that alters or restricts the applicable version of this License or the recipients' rights hereunder. However, You may include an additional document offering the additional rights described in Section 3.5.
+
+ &lt;&lt;var;name="bullet";original="3.2.";match=".{0,20}"&gt;&gt; Availability of Source Code.
+
+ Any Modification which You create or to which You contribute must be made available in Source Code form under the terms of this License either on the same media as an Executable version or via an accepted Electronic Distribution Mechanism to anyone to whom you made an Executable version available; and if made available via Electronic Distribution Mechanism, must remain available for at least twelve (12) months after the date it initially became available, or at least six (6) months after a subsequent version of that particular Modification has been made available to such recipients. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party.
+
+ &lt;&lt;var;name="bullet";original="3.3.";match=".{0,20}"&gt;&gt; Description of Modifications.
+
+ You must cause all Covered Code to which you contribute to contain a file documenting the changes You made to create that Covered Code and the date of any change. You must include a prominent statement that the Modification is derived, directly or indirectly, from Original Code provided by the Initial Developer and including the name of the Initial Developer in (a) the Source Code, and (b) in any notice in an Executable version or related documentation in which You describe the origin or ownership of the Covered Code.
+
+ &lt;&lt;var;name="bullet";original="3.4.";match=".{0,20}"&gt;&gt; Intellectual Property Matters
+
+ &lt;&lt;var;name="bullet";original="(a)";match=".{0,20}"&gt;&gt; Third Party Claims.
+
+ If You have knowledge that a party claims an intellectual property right in particular functionality or code (or its utilization under this License), you must include a text file with the source code distribution titled "LEGAL" which describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If you obtain such knowledge after You make Your Modification available as described in Section 3.2, You shall promptly modify the LEGAL file in all copies You make available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Covered Code that new knowledge has been obtained.
+
+ &lt;&lt;var;name="bullet";original="(b)";match=".{0,20}"&gt;&gt; Contributor APIs.
+
+ If Your Modification is an application programming interface and You own or control patents which are reasonably necessary to implement that API, you must also include this information in the LEGAL file.
+
+ &lt;&lt;var;name="bullet";original="3.5.";match=".{0,20}"&gt;&gt; Required Notices.
+
+ You must duplicate the notice in Exhibit A in each file of the Source Code, and this License in any documentation for the Source Code, where You describe recipients' rights relating to Covered Code. If You created one or more Modification(s), You may add your name as a Contributor to the notice described in Exhibit A. If it is not possible to put such notice in a particular Source Code file due to its structure, then you must include such notice in a location (such as a relevant directory file) where a user would be likely to look for such a notice. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Code. However, You may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear than any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.
+
+ &lt;&lt;var;name="bullet";original="3.6.";match=".{0,20}"&gt;&gt; Distribution of Executable Versions.
+
+ You may distribute Covered Code in Executable form only if the requirements of Section 3.1-3.5 have been met for that Covered Code, and if You include a notice stating that the Source Code version of the Covered Code is available under the terms of this License, including a description of how and where You have fulfilled the obligations of Section 3.2. The notice must be conspicuously included in any notice in an Executable version, related documentation or collateral in which You describe recipients' rights relating to the Covered Code. You may distribute the Executable version of Covered Code under a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable version does not attempt to limit or alter the recipient's rights in the Source Code version from the rights set forth in this License. If You distribute the Executable version under a different license You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or any Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.
+
+ &lt;&lt;var;name="bullet";original="3.7.";match=".{0,20}"&gt;&gt; Larger Works.
+
+ You may create a Larger Work by combining Covered Code with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Code.
+
+ &lt;&lt;var;name="bullet";original="4.";match=".{0,20}"&gt;&gt; Inability to Comply Due to Statute or Regulation.
+
+ If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Code due to statute or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 3.4 and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.
+
+ &lt;&lt;var;name="bullet";original="5.";match=".{0,20}"&gt;&gt; Application of this License.
+
+ This License applies to code to which the Initial Developer has attached the notice in Exhibit A, and to related Covered Code.
+
+ &lt;&lt;var;name="bullet";original="6.";match=".{0,20}"&gt;&gt; Versions of the License.
+
+ &lt;&lt;var;name="bullet";original="6.1.";match=".{0,20}"&gt;&gt; New Versions.
+
+ Netscape Communications Corporation ("Netscape") may publish revised and/or new versions of the License from time to time. Each version will be given a distinguishing version number.
+
+ &lt;&lt;var;name="bullet";original="6.2.";match=".{0,20}"&gt;&gt; Effect of New Versions.
+
+ Once Covered Code has been published under a particular version of the License, You may always continue to use it under the terms of that version. You may also choose to use such Covered Code under the terms of any subsequent version of the License published by Netscape. No one other than Netscape has the right to modify the terms applicable to Covered Code created under this License.
+
+ &lt;&lt;var;name="bullet";original="6.3.";match=".{0,20}"&gt;&gt; Derivative Works.
+
+ If you create or use a modified version of this License (which you may only do in order to apply it to code which is not already Covered Code governed by this License), you must (a) rename Your license so that the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", "NPL" or any confusingly similar phrase do not appear anywhere in your license and (b) otherwise make it clear that your version of the license contains terms which differ from the Mozilla Public License and Netscape Public License. (Filling in the name of the Initial Developer, Original Code or Contributor in the notice described in Exhibit A shall not of themselves be deemed to be modifications of this License.)
+
+ &lt;&lt;var;name="bullet";original="7.";match=".{0,20}"&gt;&gt; DISCLAIMER OF WARRANTY.
+
+ COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+ &lt;&lt;var;name="bullet";original="8.";match=".{0,20}"&gt;&gt; TERMINATION.
+
+ This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. All sublicenses to the Covered Code which are properly granted shall survive any termination of this License. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.
+
+ &lt;&lt;var;name="bullet";original="9.";match=".{0,20}"&gt;&gt; LIMITATION OF LIABILITY.
+
+ UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO YOU OR ANY OTHER PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THAT EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
+
+ &lt;&lt;var;name="bullet";original="10.";match=".{0,20}"&gt;&gt; U.S. GOVERNMENT END USERS.
+
+ The Covered Code is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" and "commercial computer software documentation," as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Code with only those rights set forth herein.
+
+ &lt;&lt;var;name="bullet";original="11.";match=".{0,20}"&gt;&gt; MISCELLANEOUS.
+
+ This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. With respect to disputes in which at least one party is a citizen of, or an entity chartered or registered to do business in, the United States of America: (a) unless otherwise agreed in writing, all disputes relating to this License (excepting any dispute relating to intellectual property rights) shall be subject to final and binding arbitration, with the losing party paying all costs of arbitration; (b) any arbitration relating to this Agreement shall be held in Santa Clara County, California, under the auspices of JAMS/EndDispute; and (c) any litigation relating to this Agreement shall be subject to the jurisdiction of the Federal Courts of the Northern District of California, with venue lying in Santa Clara County, California, with the losing party responsible for costs, including without limitation, court costs and reasonable attorneys fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License.
+
+ &lt;&lt;var;name="bullet";original="12.";match=".{0,20}"&gt;&gt; RESPONSIBILITY FOR CLAIMS.
+
+ Except in cases where another Contributor has failed to comply with Section 3.4, You are responsible for damages arising, directly or indirectly, out of Your utilization of rights under this License, based on the number of copies of Covered Code you made available, the revenues you received from utilizing such rights, and other relevant factors. You agree to work with affected parties to distribute responsibility on an equitable basis.&lt;&lt;beginOptional&gt;&gt; EXHIBIT A.
+
+&lt;&lt;beginOptional&gt;&gt;"&lt;&lt;endOptional&gt;&gt;The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
+
+The Original Code is &lt;&lt;var;name="code";original="_____";match=".+"&gt;&gt; . The Initial Developer of the Original Code is &lt;&lt;var;name="InitialDeveloper";original="_____";match=".+"&gt;&gt; . Portions created by &lt;&lt;var;name="createdby";original="_____";match=".+"&gt;&gt; are Copyright (C) &lt;&lt;var;name="copyright";original="_____";match=".+"&gt;&gt; . All Rights Reserved. Contributor(s): &lt;&lt;var;name="contributor";original="_____";match=".+"&gt;&gt; .&lt;&lt;beginOptional&gt;&gt;"&lt;&lt;endOptional&gt;&gt;&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseTemplate>
+ <spdx:licenseId>MPL-1.0</spdx:licenseId>
+ <spdx:standardLicenseHeader
+ >"The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
+
+Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License.
+
+The Original Code is _____ . The Initial Developer of the Original Code is _____ . Portions created by _____ are Copyright (C) _____ . All Rights Reserved. Contributor(s): _____ ."</spdx:standardLicenseHeader>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ </spdx:License>
+ </spdx:licenseConcluded>
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/licenses/MPL-1.0"/>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:copyrightText>Copyright 2008-2010 John Smith</spdx:copyrightText>
+ <spdx:description>The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.</spdx:description>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:fileName>./lib-source/jena-2.6.3-sources.jar</spdx:fileName>
+ </spdx:File>
+ </spdx:fileDependency>
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-CommonsLangSrc"/>
+ </spdx:File>
+ </j.0:reference>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+ </spdx:range>
+ <spdx:snippetFromFile rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/licenses/GPL-2.0-only"/>
+ <spdx:range>
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>23</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>5</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+ </spdx:range>
+ <rdfs:comment>This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.</rdfs:comment>
+ <spdx:licenseConcluded>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0">
+ <spdx:standardLicenseTemplate>&lt;&lt;beginOptional&gt;&gt; GNU GENERAL PUBLIC LICENSE
+
+Version 2, June 1991&lt;&lt;endOptional&gt;&gt;
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. &lt;&lt;var;name="incComma";original="";match=",|"&gt;&gt;
+
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301&lt;&lt;beginOptional&gt;&gt;, &lt;&lt;endOptional&gt;&gt; USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+&lt;&lt;var;name="termsTitle";original="";match="GNU GENERAL PUBLIC LICENSE|"&gt;&gt; TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ &lt;&lt;var;name="bullet";original="0.";match=".{0,20}"&gt;&gt; This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
+
+ Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
+
+ &lt;&lt;var;name="bullet";original="1.";match=".{0,20}"&gt;&gt; You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
+
+ You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
+
+ &lt;&lt;var;name="bullet";original="2.";match=".{0,20}"&gt;&gt; You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
+
+ &lt;&lt;var;name="bullet";original="c)";match=".{0,20}"&gt;&gt; If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
+
+ These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
+
+ Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
+
+ In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
+
+ &lt;&lt;var;name="bullet";original="3.";match=".{0,20}"&gt;&gt; You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
+
+ &lt;&lt;var;name="bullet";original="a)";match=".{0,20}"&gt;&gt; Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ &lt;&lt;var;name="bullet";original="b)";match=".{0,20}"&gt;&gt; Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
+
+ &lt;&lt;var;name="bullet";original="c)";match=".{0,20}"&gt;&gt; Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
+
+ The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
+
+ If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
+
+ &lt;&lt;var;name="bullet";original="4.";match=".{0,20}"&gt;&gt; You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
+
+ &lt;&lt;var;name="bullet";original="5.";match=".{0,20}"&gt;&gt; You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
+
+ &lt;&lt;var;name="bullet";original="6.";match=".{0,20}"&gt;&gt; Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
+
+ &lt;&lt;var;name="bullet";original="7.";match=".{0,20}"&gt;&gt; If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
+
+ If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
+
+ It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
+
+ This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
+
+ &lt;&lt;var;name="bullet";original="8.";match=".{0,20}"&gt;&gt; If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
+
+ &lt;&lt;var;name="bullet";original="9.";match=".{0,20}"&gt;&gt; The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
+
+ &lt;&lt;var;name="bullet";original="10.";match=".{0,20}"&gt;&gt; If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ &lt;&lt;var;name="bullet";original="11.";match=".{0,20}"&gt;&gt; BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ &lt;&lt;var;name="bullet";original="12.";match=".{0,20}"&gt;&gt; IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.&lt;&lt;beginOptional&gt;&gt; END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
+
+&lt;&lt;beginOptional&gt;&gt;&lt;&lt;&lt;endOptional&gt;&gt;one line to give the program's name and &lt;&lt;var;name="ideaArticle";original="an";match="a brief|an"&gt;&gt; idea of what it does.&lt;&lt;beginOptional&gt;&gt;&gt;&lt;&lt;endOptional&gt;&gt;
+
+Copyright (C)&lt;&lt;beginOptional&gt;&gt; &lt;&lt;&lt;endOptional&gt;&gt; &lt;&lt;var;name="templateYear";original="yyyy";match="yyyy|year"&gt;&gt;&lt;&lt;beginOptional&gt;&gt;&gt; &lt;&lt;endOptional&gt;&gt;&lt;&lt;beginOptional&gt;&gt; &lt;&lt;&lt;endOptional&gt;&gt;name of author&lt;&lt;beginOptional&gt;&gt;&gt;&lt;&lt;endOptional&gt;&gt;
+
+This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301&lt;&lt;beginOptional&gt;&gt;, &lt;&lt;endOptional&gt;&gt; USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
+
+Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
+
+Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+&lt;&lt;beginOptional&gt;&gt;&lt;&lt;&lt;endOptional&gt;&gt;signature of Ty Coon&lt;&lt;beginOptional&gt;&gt;&gt;&lt;&lt;endOptional&gt;&gt;, 1 April 1989 Ty Coon, President of Vice&lt;&lt;endOptional&gt;&gt;&lt;&lt;beginOptional&gt;&gt; This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseTemplate>
+ <spdx:standardLicenseHeaderTemplate>Copyright (C) &lt;&lt;var;name="copyright";original="yyyy name of author";match=".+"&gt;&gt;
+
+This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301&lt;&lt;beginOptional&gt;&gt;, &lt;&lt;endOptional&gt;&gt; USA.</spdx:standardLicenseHeaderTemplate>
+ <rdfs:seeAlso>https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html</rdfs:seeAlso>
+ <spdx:licenseId>GPL-2.0</spdx:licenseId>
+ <spdx:standardLicenseHeader>Copyright (C) yyyy name of author
+
+This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.</spdx:standardLicenseHeader>
+ <spdx:name>GNU General Public License v2.0 only</spdx:name>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <spdx:licenseText>GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) &lt; yyyy&gt; This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.</spdx:licenseText>
+ <rdfs:seeAlso>https://opensource.org/licenses/GPL-2.0</rdfs:seeAlso>
+ <spdx:isDeprecatedLicenseId>true</spdx:isDeprecatedLicenseId>
+ </spdx:License>
+ </spdx:licenseConcluded>
+ </spdx:Snippet>
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DOCUMENT">
+ <spdx:specVersion>SPDX-2.2</spdx:specVersion>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:fileName>./package/foo.c</spdx:fileName>
+ <spdx:licenseComments>The concluded license was taken from the package level that the file was included in.</spdx:licenseComments>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>File level annotation</rdfs:comment>
+ <spdx:annotator>Person: File Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-2"/>
+ <spdx:checksum rdf:nodeID="A0"/>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/GPL-2.0-only"/>
+ <spdx:copyrightText>Copyright 2008-2010 John Smith</spdx:copyrightText>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ <spdx:fileContributor>The Regents of the University of California</spdx:fileContributor>
+ <spdx:noticeText>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.</spdx:noticeText>
+ <spdx:fileContributor>Modified by Paul Mundt lethal@linux-sh.org</spdx:fileContributor>
+ <spdx:fileContributor>IBM Corporation</spdx:fileContributor>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:licenseConcluded>
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-2"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0-only"/>
+ </spdx:DisjunctiveLicenseSet>
+ </spdx:licenseConcluded>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2758</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <rdfs:comment>The concluded license was taken from the package level that the file was included in.
+This information was found in the COPYING.txt file in the xyz directory.</rdfs:comment>
+ </spdx:File>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:dataLicense>
+ <spdx:License rdf:about="http://spdx.org/licenses/CC0-1.0">
+ <spdx:standardLicenseTemplate>&lt;&lt;beginOptional&gt;&gt; &lt;&lt;beginOptional&gt;&gt; Creative Commons&lt;&lt;beginOptional&gt;&gt; Legal Code&lt;&lt;endOptional&gt;&gt;&lt;&lt;endOptional&gt;&gt;
+
+CC0 1.0 Universal&lt;&lt;endOptional&gt;&gt;&lt;&lt;beginOptional&gt;&gt; CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.&lt;&lt;endOptional&gt;&gt;
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
+
+ &lt;&lt;var;name="bullet";original="1.";match=".{0,20}"&gt;&gt; Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
+
+ &lt;&lt;var;name="bullet";original="i.";match=".{0,20}"&gt;&gt; the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
+
+ &lt;&lt;var;name="bullet";original="ii.";match=".{0,20}"&gt;&gt; moral rights retained by the original author(s) and/or performer(s);
+
+ &lt;&lt;var;name="bullet";original="iii.";match=".{0,20}"&gt;&gt; publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
+
+ &lt;&lt;var;name="bullet";original="iv.";match=".{0,20}"&gt;&gt; rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
+
+ &lt;&lt;var;name="bullet";original="v.";match=".{0,20}"&gt;&gt; rights protecting the extraction, dissemination, use and reuse of data in a Work;
+
+ &lt;&lt;var;name="bullet";original="vi.";match=".{0,20}"&gt;&gt; database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
+
+ &lt;&lt;var;name="bullet";original="vii.";match=".{0,20}"&gt;&gt; other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
+
+ &lt;&lt;var;name="bullet";original="2.";match=".{0,20}"&gt;&gt; Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
+
+ &lt;&lt;var;name="bullet";original="3.";match=".{0,20}"&gt;&gt; Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
+
+ &lt;&lt;var;name="bullet";original="4.";match=".{0,20}"&gt;&gt; Limitations and Disclaimers.
+
+ &lt;&lt;var;name="bullet";original="a.";match=".{0,20}"&gt;&gt; No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
+
+ &lt;&lt;var;name="bullet";original="b.";match=".{0,20}"&gt;&gt; Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
+
+ &lt;&lt;var;name="bullet";original="c.";match=".{0,20}"&gt;&gt; Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
+
+ &lt;&lt;var;name="bullet";original="d.";match=".{0,20}"&gt;&gt; Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.&lt;&lt;beginOptional&gt;&gt; &lt;&lt;var;name="upstreamLink";original="";match="For more information, please see &lt;http://creativecommons.org/publicdomain/zero/1.0/&gt;"&gt;&gt;&lt;&lt;endOptional&gt;&gt;</spdx:standardLicenseTemplate>
+ <spdx:licenseText>Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.</spdx:licenseText>
+ <rdfs:seeAlso>https://creativecommons.org/publicdomain/zero/1.0/legalcode</rdfs:seeAlso>
+ <spdx:name>Creative Commons Zero v1.0 Universal</spdx:name>
+ <spdx:licenseId>CC0-1.0</spdx:licenseId>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ </spdx:License>
+ </spdx:dataLicense>
+ <spdx:hasExtractedLicensingInfo rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-3"/>
+ <spdx:hasExtractedLicensingInfo>
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-Beerware-4.2">
+ <spdx:extractedText>"THE BEER-WARE LICENSE" (Revision 42):
+phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp &lt;/
+LicenseName: Beer-Ware License (Version 42)
+LicenseCrossReference: http://people.freebsd.org/~phk/
+LicenseComment:
+The beerware license has a couple of other standard variants.</spdx:extractedText>
+ <spdx:licenseId>LicenseRef-Beerware-4.2</spdx:licenseId>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:hasExtractedLicensingInfo>
+ <spdx:hasExtractedLicensingInfo>
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-4">
+ <spdx:extractedText>/*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</spdx:extractedText>
+ <spdx:licenseId>LicenseRef-4</spdx:licenseId>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:hasExtractedLicensingInfo>
+ <spdx:reviewed>
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ </spdx:reviewed>
+ <spdx:hasExtractedLicensingInfo rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-2"/>
+ <rdfs:comment>This document was created using SPDX 2.0 using licenses from the web site.</rdfs:comment>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_contains"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Package"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Package"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_copyOf"/>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-ToolsElement"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe ()</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ <spdx:name>SPDX-Tools-v2.0</spdx:name>
+ <spdx:creationInfo>
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>1.19</spdx:licenseListVersion>
+ <spdx:created>2010-01-29T18:30:22Z</spdx:created>
+ <spdx:creator>Organization: ExampleCodeInspect ()</spdx:creator>
+ <spdx:creator>Tool: LicenseFind-1.0</spdx:creator>
+ <rdfs:comment>This package has been shipped in source and binary form.
+The binaries were created with gcc 4.5.1 and expect to link to
+compatible system run time libraries.</rdfs:comment>
+ <spdx:creator>Person: Jane Doe ()</spdx:creator>
+ </spdx:CreationInfo>
+ </spdx:creationInfo>
+ <spdx:reviewed>
+ <spdx:Review>
+ <rdfs:comment>This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses</rdfs:comment>
+ <spdx:reviewDate>2010-02-10T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Joe Reviewer</spdx:reviewer>
+ </spdx:Review>
+ </spdx:reviewed>
+ <spdx:hasExtractedLicensingInfo rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LicenseRef-1"/>
+ </spdx:SpdxDocument>
+</rdf:RDF> \ No newline at end of file
diff --git a/examples/sample-docs/tv/SPDXTagExample-v2.2.spdx b/examples/sample-docs/tv/SPDXTagExample-v2.2.spdx
new file mode 100644
index 0000000..e8f32eb
--- /dev/null
+++ b/examples/sample-docs/tv/SPDXTagExample-v2.2.spdx
@@ -0,0 +1,329 @@
+SPDXVersion: SPDX-2.2
+DataLicense: CC0-1.0
+DocumentNamespace: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
+DocumentName: SPDX-Tools-v2.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentComment: <text>This document was created using SPDX 2.0 using licenses from the web site.</text>
+
+## External Document References
+ExternalDocumentRef: DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1: d6a770ba38583ed4bb4525bd96e50461655d2759
+## Creation Information
+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>
+LicenseListVersion: 3.9
+## Annotations
+Annotator: Person: Jane Doe ()
+AnnotationDate: 2010-01-29T18:30:22Z
+AnnotationComment: <text>Document level annotation</text>
+AnnotationType: OTHER
+SPDXREF: SPDXRef-DOCUMENT
+Annotator: Person: Joe Reviewer
+AnnotationDate: 2010-02-10T00:00:00Z
+AnnotationComment: <text>This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses</text>
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+Annotator: Person: Suzanne Reviewer
+AnnotationDate: 2011-03-13T00:00:00Z
+AnnotationComment: <text>Another example reviewer.</text>
+AnnotationType: REVIEW
+SPDXREF: SPDXRef-DOCUMENT
+## 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
+
+FileName: ./package/foo.c
+SPDXID: SPDXRef-File
+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>
+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: <text>Copyright 2008-2010 John Smith</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
+## Annotations
+Annotator: Person: File Commenter
+AnnotationDate: 2011-01-29T18:30:22Z
+AnnotationComment: <text>File level annotation</text>
+AnnotationType: OTHER
+SPDXREF: SPDXRef-File
+## Relationships
+Relationship: SPDXRef-File GENERATED_FROM SPDXRef-fromDoap-0
+## Package Information
+PackageName: glibc
+SPDXID: 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(./package.spdx)
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageHomePage: http://ftp.gnu.org/gnu/glibc
+PackageSourceInfo: <text>uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.</text>
+PackageLicenseConcluded: (LGPL-2.0-only OR LicenseRef-3)
+## License information from files
+PackageLicenseInfoFromFiles: GPL-2.0-only
+PackageLicenseInfoFromFiles: LicenseRef-2
+PackageLicenseInfoFromFiles: LicenseRef-1
+PackageLicenseDeclared: (LGPL-2.0-only AND LicenseRef-3)
+PackageLicenseComments: <text>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.</text>
+PackageCopyrightText: <text>Copyright 2008-2010 John Smith</text>
+PackageSummary: <text>GNU C library.</text>
+PackageDescription: <text>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.</text>
+PackageAttributionText: <text>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.</text>
+ExternalRef: SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*
+ExternalRef: OTHER LocationRef-acmeforge acmecorp/acmenator/4.1.3-alpha
+ExternalRefComment: This is the external ref for Acme
+## Annotations
+Annotator: Person: Package Commenter
+AnnotationDate: 2011-01-29T18:30:22Z
+AnnotationComment: <text>Package level annotation</text>
+AnnotationType: OTHER
+SPDXREF: SPDXRef-Package
+## Relationships
+Relationship: SPDXRef-Package CONTAINS SPDXRef-JenaLib
+Relationship: SPDXRef-Package DYNAMIC_LINK SPDXRef-Saxon
+
+## File Information
+FileName: ./lib-source/commons-lang3-3.1-sources.jar
+SPDXID: SPDXRef-CommonsLangSrc
+FileComment: <text>This file is used by Jena</text>
+FileType: ARCHIVE
+FileChecksum: SHA1: c2b4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: <text>Copyright 2001-2011 The Apache Software Foundation</text>
+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
+## Relationships
+Relationship: SPDXRef-CommonsLangSrc GENERATED_FROM NOASSERTION
+
+FileName: ./lib-source/jena-2.6.3-sources.jar
+SPDXID: SPDXRef-JenaLib
+FileComment: <text>This file belongs to Jena</text>
+FileType: ARCHIVE
+FileChecksum: SHA1: 3ab4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: LicenseRef-1
+LicenseInfoInFile: LicenseRef-1
+LicenseComments: This license is used by Jena
+FileCopyrightText: <text>(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP</text>
+FileContributor: Apache Software Foundation
+FileContributor: Hewlett Packard Inc.
+## Relationships
+Relationship: SPDXRef-JenaLib CONTAINS SPDXRef-Package
+
+FileName: ./src/org/spdx/parser/DOAPProject.java
+SPDXID: SPDXRef-DoapSource
+FileType: SOURCE
+FileChecksum: SHA1: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: <text>Copyright 2010, 2011 Source Auditor Inc.</text>
+FileContributor: Protecode Inc.
+FileContributor: SPDX Technical Team Members
+FileContributor: Open Logic Inc.
+FileContributor: Source Auditor Inc.
+FileContributor: Black Duck Software In.c
+
+## Package Information
+PackageName: Apache Commons Lang
+SPDXID: SPDXRef-fromDoap-1
+PackageDownloadLocation: NOASSERTION
+PackageHomePage: http://commons.apache.org/proper/commons-lang/
+PackageLicenseConcluded: NOASSERTION
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: <text>NOASSERTION</text>
+FilesAnalyzed: false
+
+## Package Information
+PackageName: Jena
+SPDXID: 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
+PackageHomePage: http://www.openjena.org/
+PackageLicenseConcluded: NOASSERTION
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: <text>NOASSERTION</text>
+ExternalRef: PACKAGE-MANAGER purl pkg:maven/org.apache.jena/apache-jena@3.12.0
+FilesAnalyzed: false
+
+## Package Information
+PackageName: Saxon
+SPDXID: 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
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageHomePage: http://saxon.sourceforge.net/
+PackageLicenseConcluded: MPL-1.0
+PackageLicenseDeclared: MPL-1.0
+PackageLicenseComments: <text>Other versions available for a commercial license</text>
+PackageCopyrightText: <text>Copyright Saxonica Ltd</text>
+PackageDescription: <text>The Saxon package is a collection of tools for processing XML documents.</text>
+FilesAnalyzed: false
+
+## Snippet Information
+SnippetSPDXID: SPDXRef-Snippet
+SnippetFromFileSPDXID: SPDXRef-DoapSource
+SnippetByteRange: 310:420
+SnippetLineRange: 5:23
+SnippetLicenseConcluded: GPL-2.0-only
+LicenseInfoInSnippet: 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
+
+
+## License Information
+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, http://justasample.url.com
+LicenseComment: <text>This is tye CyperNeko License</text>
+
diff --git a/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx b/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx
new file mode 100644
index 0000000..7c5ae3d
--- /dev/null
+++ b/examples/sample-docs/tv/SPDXTagExample-v2.3.spdx
@@ -0,0 +1,328 @@
+SPDXVersion: SPDX-2.3
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-SPDXRef-DOCUMENT
+DocumentName: SPDX-Tools-v2.0
+DocumentNamespace: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
+ExternalDocumentRef: DocumentRef-DocumentRef-spdx-tool-1.2 http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1:d6a770ba38583ed4bb4525bd96e50461655d2759
+DocumentComment: This document was created using SPDX 2.0 using licenses from the web site.
+LicenseListVersion: 3.9
+Creator: Tool: LicenseFind-1.0
+Creator: Organization: ExampleCodeInspect ()
+Creator: Person: Jane Doe ()
+Created: 2010-01-29T18:30:22Z
+CreatorComment: <text>This package has been shipped in source and binary form.
+The binaries were created with gcc 4.5.1 and expect to link to
+compatible system run time libraries.</text>
+
+##### Unpackaged files
+
+FileName: ./lib-source/commons-lang3-3.1-sources.jar
+SPDXID: SPDXRef-SPDXRef-CommonsLangSrc
+FileType: ARCHIVE
+FileChecksum: SHA1: c2b4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright 2001-2011 The Apache Software Foundation
+FileComment: This file is used by Jena
+FileNotice: <text>Apache Commons Lang
+Copyright 2001-2011 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software from the Spring Framework,
+under the Apache License 2.0 (see: StringUtils.containsWhitespace())</text>
+FileContributor: Apache Software Foundation
+
+FileName: ./src/org/spdx/parser/DOAPProject.java
+SPDXID: SPDXRef-SPDXRef-DoapSource
+FileType: SOURCE
+FileChecksum: SHA1: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
+LicenseConcluded: Apache-2.0
+LicenseInfoInFile: Apache-2.0
+FileCopyrightText: Copyright 2010, 2011 Source Auditor Inc.
+FileContributor: Protecode Inc.
+FileContributor: SPDX Technical Team Members
+FileContributor: Open Logic Inc.
+FileContributor: Source Auditor Inc.
+FileContributor: Black Duck Software In.c
+
+FileName: ./package/foo.c
+SPDXID: SPDXRef-SPDXRef-File
+FileType: SOURCE
+FileChecksum: SHA1: d6a770ba38583ed4bb4525bd96e50461655d2758
+FileChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+LicenseConcluded: (LGPL-2.0-only OR LicenseRef-2)
+LicenseInfoInFile: GPL-2.0-only
+LicenseInfoInFile: LicenseRef-2
+LicenseComments: The concluded license was taken from the package level that the file was included in.
+FileCopyrightText: Copyright 2008-2010 John Smith
+FileComment: <text>The concluded license was taken from the package level that the file was included in.
+This information was found in the COPYING.txt file in the xyz directory.</text>
+FileNotice: <text>Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</text>
+FileContributor: The Regents of the University of California
+FileContributor: Modified by Paul Mundt lethal@linux-sh.org
+FileContributor: IBM Corporation
+
+FileName: ./lib-source/jena-2.6.3-sources.jar
+SPDXID: SPDXRef-SPDXRef-JenaLib
+FileType: ARCHIVE
+FileChecksum: SHA1: 3ab4e1c67a2d28fced849ee1bb76e7391b93f125
+LicenseConcluded: LicenseRef-1
+LicenseInfoInFile: LicenseRef-1
+LicenseComments: This license is used by Jena
+FileCopyrightText: (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+FileComment: This file belongs to Jena
+FileContributor: Apache Software Foundation
+FileContributor: Hewlett Packard Inc.
+
+##### Package: centos
+
+PackageName: centos
+SPDXID: SPDXRef-SPDXRef-CentOS-7
+PackageVersion: centos7.9.2009
+PackageFileName: saxonB-8.8.zip
+PackageDownloadLocation: NOASSERTION
+PrimaryPackagePurpose: CONTAINER
+ReleaseDate: 2021-10-15T02:38:00Z
+BuiltDate: 2021-09-15T02:38:00Z
+ValidUntilDate: 2022-10-15T02:38:00Z
+FilesAnalyzed: false
+PackageHomePage: https://www.centos.org/
+PackageCopyrightText: NOASSERTION
+PackageDescription: The CentOS container used to run the application.
+
+##### Package: glibc
+
+PackageName: glibc
+SPDXID: SPDXRef-SPDXRef-Package
+PackageVersion: 2.11.1
+PackageFileName: glibc-2.11.1.tar.gz
+PackageSupplier: Person: Jane Doe (jane.doe@example.com)
+PackageOriginator: Organization: ExampleCodeInspect (contact@example.com)
+PackageDownloadLocation: http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz
+PackageVerificationCode: d6a770ba38583ed4bb4525bd96e50461655d2758 (excludes: ./package.spdx)
+PackageChecksum: MD5: 624c1abb3664f4b35547e7c73864ad24
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageChecksum: SHA256: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+PackageHomePage: http://ftp.gnu.org/gnu/glibc
+PackageSourceInfo: uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.
+PackageLicenseConcluded: (LGPL-2.0-only OR LicenseRef-3)
+PackageLicenseInfoFromFiles: GPL-2.0-only
+PackageLicenseInfoFromFiles: LicenseRef-2
+PackageLicenseInfoFromFiles: LicenseRef-1
+PackageLicenseDeclared: (LGPL-2.0-only AND LicenseRef-3)
+PackageLicenseComments: The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.
+PackageCopyrightText: Copyright 2008-2010 John Smith
+PackageSummary: GNU C library.
+PackageDescription: The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.
+ExternalRef: SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*
+ExternalRef: OTHER http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge acmecorp/acmenator/4.1.3-alpha
+ExternalRefComment: This is the external ref for Acme
+PackageAttributionText: The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.
+
+##### Package: Saxon
+
+PackageName: Saxon
+SPDXID: SPDXRef-SPDXRef-Saxon
+PackageVersion: 8.8
+PackageFileName: saxonB-8.8.zip
+PackageDownloadLocation: https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download
+FilesAnalyzed: false
+PackageChecksum: SHA1: 85ed0817af83a24ad8da68c2b5094de69833983c
+PackageHomePage: http://saxon.sourceforge.net/
+PackageLicenseConcluded: MPL-1.0
+PackageLicenseDeclared: MPL-1.0
+PackageLicenseComments: Other versions available for a commercial license
+PackageCopyrightText: Copyright Saxonica Ltd
+PackageDescription: The Saxon package is a collection of tools for processing XML documents.
+
+##### Package: Jena
+
+PackageName: Jena
+SPDXID: SPDXRef-SPDXRef-fromDoap-0
+PackageVersion: 3.12.0
+PackageDownloadLocation: https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz
+FilesAnalyzed: false
+PackageHomePage: http://www.openjena.org/
+PackageLicenseConcluded: NOASSERTION
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: NOASSERTION
+ExternalRef: PACKAGE-MANAGER purl pkg:maven/org.apache.jena/apache-jena@3.12.0
+
+##### Package: Apache Commons Lang
+
+PackageName: Apache Commons Lang
+SPDXID: SPDXRef-SPDXRef-fromDoap-1
+PackageDownloadLocation: NOASSERTION
+FilesAnalyzed: false
+PackageHomePage: http://commons.apache.org/proper/commons-lang/
+PackageLicenseDeclared: NOASSERTION
+PackageCopyrightText: NOASSERTION
+
+##### Other Licenses
+
+LicenseID: LicenseRef-1
+ExtractedText: <text>/*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</text>
+
+LicenseID: LicenseRef-2
+ExtractedText: <text>This package includes the GRDDL parser developed by Hewlett Packard under the following license:
+� Copyright 2007 Hewlett-Packard Development Company, LP
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</text>
+
+LicenseID: LicenseRef-4
+ExtractedText: <text>/*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</text>
+
+LicenseID: LicenseRef-Beerware-4.2
+ExtractedText: <text>"THE BEER-WARE LICENSE" (Revision 42):
+phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp</text>
+LicenseName: Beer-Ware License (Version 42)
+LicenseCrossReference: http://people.freebsd.org/~phk/
+LicenseComment: The beerware license has a couple of other standard variants.
+
+LicenseID: LicenseRef-3
+ExtractedText: <text>The CyberNeko Software License, Version 1.0
+
+
+(C) Copyright 2002-2005, Andy Clark. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+3. The end-user documentation included with the redistribution,
+ if any, must include the following acknowledgment:
+ "This product includes software developed by Andy Clark."
+ Alternately, this acknowledgment may appear in the software itself,
+ if and wherever such third-party acknowledgments normally appear.
+
+4. The names "CyberNeko" and "NekoHTML" must not be used to endorse
+ or promote products derived from this software without prior
+ written permission. For written permission, please contact
+ andyc@cyberneko.net.
+
+5. Products derived from this software may not be called "CyberNeko",
+ nor may "CyberNeko" appear in their name, without prior written
+ permission of the author.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
+OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</text>
+LicenseName: CyberNeko License
+LicenseCrossReference: http://people.apache.org/~andyc/neko/LICENSE
+LicenseCrossReference: http://justasample.url.com
+LicenseComment: This is tye CyperNeko License
+
+##### Relationships
+
+Relationship: SPDXRef-DOCUMENT CONTAINS SPDXRef-Package
+Relationship: SPDXRef-DOCUMENT COPY_OF DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-File
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package
+Relationship: SPDXRef-Package CONTAINS SPDXRef-JenaLib
+Relationship: SPDXRef-Package DYNAMIC_LINK SPDXRef-Saxon
+Relationship: SPDXRef-CommonsLangSrc GENERATED_FROM NOASSERTION
+Relationship: SPDXRef-JenaLib CONTAINS SPDXRef-Package
+Relationship: SPDXRef-File GENERATED_FROM SPDXRef-fromDoap-0
+
+##### Annotations
+
+Annotator: Person: Jane Doe ()
+AnnotationDate: 2010-01-29T18:30:22Z
+AnnotationType: OTHER
+AnnotationComment: Document level annotation
+
+Annotator: Person: Joe Reviewer
+AnnotationDate: 2010-02-10T00:00:00Z
+AnnotationType: REVIEW
+AnnotationComment: This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses
+
+Annotator: Person: Suzanne Reviewer
+AnnotationDate: 2011-03-13T00:00:00Z
+AnnotationType: REVIEW
+AnnotationComment: Another example reviewer.
+
+##### Reviews
+
+Reviewer: Person: joe@example.com
+ReviewDate: 2021-11-03T05:43:21Z
+ReviewComment: This is a review comment
+
diff --git a/examples/sample-docs/tv/hello-modified.spdx b/examples/sample-docs/tv/hello-modified.spdx
new file mode 100644
index 0000000..b520375
--- /dev/null
+++ b/examples/sample-docs/tv/hello-modified.spdx
@@ -0,0 +1,57 @@
+SPDXVersion: SPDX-2.2
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: hello-modified
+DocumentNamespace: https://swinslow.net/sampledocs/hello-modified
+Creator: Person: Steve Winslow (steve@swinslow.net)
+Creator: Tool: github.com/spdx/tools-golang/builder
+Creator: Tool: github.com/spdx/tools-golang/idsearcher
+Created: 2022-03-30T21:23:00Z
+
+##### Package: hello
+
+PackageName: hello
+SPDXID: SPDXRef-Package-hello
+PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example1/content
+FilesAnalyzed: true
+PackageVerificationCode: 9d20237bb72087e87069f96afb41c6ca2fa2a342
+PackageLicenseConcluded: GPL-3.0-or-later AND MIT
+PackageLicenseInfoFromFiles: GPL-3.0-or-later
+PackageLicenseDeclared: GPL-3.0-or-later
+PackageCopyrightText: NOASSERTION
+
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello
+
+FileName: /build/hello
+SPDXID: SPDXRef-hello-binary
+FileType: BINARY
+FileChecksum: SHA1: 20291a81ef065ff891b537b64d4fdccaf6f5ac02
+FileChecksum: SHA256: 83a33ff09648bb5fc5272baca88cf2b59fd81ac4cc6817b86998136af368708e
+FileChecksum: MD5: 08a12c966d776864cc1eb41fd03c3c3d
+LicenseConcluded: GPL-3.0-or-later AND MIT
+LicenseInfoInFile: NOASSERTION
+FileCopyrightText: NOASSERTION
+
+FileName: /src/Makefile
+SPDXID: SPDXRef-Makefile
+FileType: SOURCE
+FileChecksum: SHA1: 69a2e85696fff1865c3f0686d6c3824b59915c80
+FileChecksum: SHA256: 5da19033ba058e322e21c90e6d6d859c90b1b544e7840859c12cae5da005e79c
+FileChecksum: MD5: 559424589a4f3f75fd542810473d8bc1
+LicenseConcluded: GPL-3.0-or-later
+LicenseInfoInFile: GPL-3.0-or-later
+FileCopyrightText: NOASSERTION
+
+FileName: /src/hello.c
+SPDXID: SPDXRef-hello-src
+FileType: SOURCE
+FileChecksum: SHA1: 20862a6d08391d07d09344029533ec644fac6b21
+FileChecksum: SHA256: b4e5ca56d1f9110ca94ed0bf4e6d9ac11c2186eb7cd95159c6fdb50e8db5a823
+FileChecksum: MD5: 935054fe899ca782e11003bbae5e166c
+LicenseConcluded: GPL-3.0-or-later AND MIT
+LicenseInfoInFile: GPL-3.0-or-later
+FileCopyrightText: Copyright Contributors to the spdx-examples project.
+
+Relationship: SPDXRef-hello-binary GENERATED_FROM SPDXRef-hello-src
+Relationship: SPDXRef-hello-binary GENERATED_FROM SPDXRef-Makefile
+Relationship: SPDXRef-Makefile BUILD_TOOL_OF SPDXRef-Package-hello
diff --git a/examples/sample-docs/tv/hello.spdx b/examples/sample-docs/tv/hello.spdx
new file mode 100644
index 0000000..def760c
--- /dev/null
+++ b/examples/sample-docs/tv/hello.spdx
@@ -0,0 +1,57 @@
+SPDXVersion: SPDX-2.2
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: hello
+DocumentNamespace: https://swinslow.net/spdx-examples/example1/hello-v3
+Creator: Person: Steve Winslow (steve@swinslow.net)
+Creator: Tool: github.com/spdx/tools-golang/builder
+Creator: Tool: github.com/spdx/tools-golang/idsearcher
+Created: 2021-08-26T01:46:00Z
+
+##### Package: hello
+
+PackageName: hello
+SPDXID: SPDXRef-Package-hello
+PackageDownloadLocation: git+https://github.com/swinslow/spdx-examples.git#example1/content
+FilesAnalyzed: true
+PackageVerificationCode: 9d20237bb72087e87069f96afb41c6ca2fa2a342
+PackageLicenseConcluded: GPL-3.0-or-later
+PackageLicenseInfoFromFiles: GPL-3.0-or-later
+PackageLicenseDeclared: GPL-3.0-or-later
+PackageCopyrightText: NOASSERTION
+
+Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-hello
+
+FileName: /build/hello
+SPDXID: SPDXRef-hello-binary
+FileType: BINARY
+FileChecksum: SHA1: 20291a81ef065ff891b537b64d4fdccaf6f5ac02
+FileChecksum: SHA256: 83a33ff09648bb5fc5272baca88cf2b59fd81ac4cc6817b86998136af368708e
+FileChecksum: MD5: 08a12c966d776864cc1eb41fd03c3c3d
+LicenseConcluded: GPL-3.0-or-later
+LicenseInfoInFile: NOASSERTION
+FileCopyrightText: NOASSERTION
+
+FileName: /src/Makefile
+SPDXID: SPDXRef-Makefile
+FileType: SOURCE
+FileChecksum: SHA1: 69a2e85696fff1865c3f0686d6c3824b59915c80
+FileChecksum: SHA256: 5da19033ba058e322e21c90e6d6d859c90b1b544e7840859c12cae5da005e79c
+FileChecksum: MD5: 559424589a4f3f75fd542810473d8bc1
+LicenseConcluded: GPL-3.0-or-later
+LicenseInfoInFile: GPL-3.0-or-later
+FileCopyrightText: NOASSERTION
+
+FileName: /src/hello.c
+SPDXID: SPDXRef-hello-src
+FileType: SOURCE
+FileChecksum: SHA1: 20862a6d08391d07d09344029533ec644fac6b21
+FileChecksum: SHA256: b4e5ca56d1f9110ca94ed0bf4e6d9ac11c2186eb7cd95159c6fdb50e8db5a823
+FileChecksum: MD5: 935054fe899ca782e11003bbae5e166c
+LicenseConcluded: GPL-3.0-or-later
+LicenseInfoInFile: GPL-3.0-or-later
+FileCopyrightText: Copyright Contributors to the spdx-examples project.
+
+Relationship: SPDXRef-hello-binary GENERATED_FROM SPDXRef-hello-src
+Relationship: SPDXRef-hello-binary GENERATED_FROM SPDXRef-Makefile
+Relationship: SPDXRef-Makefile BUILD_TOOL_OF SPDXRef-Package-hello
diff --git a/examples/sample-docs/xls/SPDXSpreadsheetExample-v2.2.xlsx b/examples/sample-docs/xls/SPDXSpreadsheetExample-v2.2.xlsx
new file mode 100644
index 0000000..171ba8e
--- /dev/null
+++ b/examples/sample-docs/xls/SPDXSpreadsheetExample-v2.2.xlsx
Binary files differ
diff --git a/examples/sample-docs/xml/SPDXXMLExample-v2.2.spdx.xml b/examples/sample-docs/xml/SPDXXMLExample-v2.2.spdx.xml
new file mode 100644
index 0000000..80e0527
--- /dev/null
+++ b/examples/sample-docs/xml/SPDXXMLExample-v2.2.spdx.xml
@@ -0,0 +1,443 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<Document>
+ <SPDXID>SPDXRef-DOCUMENT</SPDXID>
+ <spdxVersion>SPDX-2.2</spdxVersion>
+ <creationInfo>
+ <comment>This package has been shipped in source and binary form.
+The binaries were created with gcc 4.5.1 and expect to link to
+compatible system run time libraries.</comment>
+ <created>2010-01-29T18:30:22Z</created>
+ <creators>Tool: LicenseFind-1.0</creators>
+ <creators>Organization: ExampleCodeInspect ()</creators>
+ <creators>Person: Jane Doe ()</creators>
+ <licenseListVersion>3.9</licenseListVersion>
+ </creationInfo>
+ <name>SPDX-Tools-v2.0</name>
+ <dataLicense>CC0-1.0</dataLicense>
+ <comment>This document was created using SPDX 2.0 using licenses from the web site.</comment>
+ <externalDocumentRefs>
+ <externalDocumentId>DocumentRef-spdx-tool-1.2</externalDocumentId>
+ <checksum>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</checksumValue>
+ </checksum>
+ <spdxDocument>http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301</spdxDocument>
+ </externalDocumentRefs>
+ <hasExtractedLicensingInfos>
+ <licenseId>LicenseRef-1</licenseId>
+ <extractedText>/*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</extractedText>
+ </hasExtractedLicensingInfos>
+ <hasExtractedLicensingInfos>
+ <licenseId>LicenseRef-2</licenseId>
+ <extractedText>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.</extractedText>
+ </hasExtractedLicensingInfos>
+ <hasExtractedLicensingInfos>
+ <licenseId>LicenseRef-4</licenseId>
+ <extractedText>/*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/</extractedText>
+ </hasExtractedLicensingInfos>
+ <hasExtractedLicensingInfos>
+ <licenseId>LicenseRef-Beerware-4.2</licenseId>
+ <comment>The beerware license has a couple of other standard variants.</comment>
+ <extractedText>"THE BEER-WARE LICENSE" (Revision 42):
+phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp</extractedText>
+ <name>Beer-Ware License (Version 42)</name>
+ <seeAlsos>http://people.freebsd.org/~phk/</seeAlsos>
+ </hasExtractedLicensingInfos>
+ <hasExtractedLicensingInfos>
+ <licenseId>LicenseRef-3</licenseId>
+ <comment>This is tye CyperNeko License</comment>
+ <extractedText>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.</extractedText>
+ <name>CyberNeko License</name>
+ <seeAlsos>http://people.apache.org/~andyc/neko/LICENSE</seeAlsos>
+ <seeAlsos>http://justasample.url.com</seeAlsos>
+ </hasExtractedLicensingInfos>
+ <annotations>
+ <annotationDate>2010-01-29T18:30:22Z</annotationDate>
+ <annotationType>OTHER</annotationType>
+ <annotator>Person: Jane Doe ()</annotator>
+ <comment>Document level annotation</comment>
+ </annotations>
+ <annotations>
+ <annotationDate>2010-02-10T00:00:00Z</annotationDate>
+ <annotationType>REVIEW</annotationType>
+ <annotator>Person: Joe Reviewer</annotator>
+ <comment>This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses</comment>
+ </annotations>
+ <annotations>
+ <annotationDate>2011-03-13T00:00:00Z</annotationDate>
+ <annotationType>REVIEW</annotationType>
+ <annotator>Person: Suzanne Reviewer</annotator>
+ <comment>Another example reviewer.</comment>
+ </annotations>
+ <documentNamespace>http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301</documentNamespace>
+ <documentDescribes>SPDXRef-File</documentDescribes>
+ <documentDescribes>SPDXRef-Package</documentDescribes>
+ <packages>
+ <SPDXID>SPDXRef-Package</SPDXID>
+ <annotations>
+ <annotationDate>2011-01-29T18:30:22Z</annotationDate>
+ <annotationType>OTHER</annotationType>
+ <annotator>Person: Package Commenter</annotator>
+ <comment>Package level annotation</comment>
+ </annotations>
+ <attributionTexts>The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.</attributionTexts>
+ <checksums>
+ <algorithm>MD5</algorithm>
+ <checksumValue>624c1abb3664f4b35547e7c73864ad24</checksumValue>
+ </checksums>
+ <checksums>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>85ed0817af83a24ad8da68c2b5094de69833983c</checksumValue>
+ </checksums>
+ <checksums>
+ <algorithm>SHA256</algorithm>
+ <checksumValue>11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd</checksumValue>
+ </checksums>
+ <copyrightText>Copyright 2008-2010 John Smith</copyrightText>
+ <description>The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.</description>
+ <downloadLocation>http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz</downloadLocation>
+ <externalRefs>
+ <referenceCategory>SECURITY</referenceCategory>
+ <referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</referenceLocator>
+ <referenceType>cpe23Type</referenceType>
+ </externalRefs>
+ <externalRefs>
+ <comment>This is the external ref for Acme</comment>
+ <referenceCategory>OTHER</referenceCategory>
+ <referenceLocator>acmecorp/acmenator/4.1.3-alpha</referenceLocator>
+ <referenceType>http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge</referenceType>
+ </externalRefs>
+ <filesAnalyzed>true</filesAnalyzed>
+ <hasFiles>SPDXRef-CommonsLangSrc</hasFiles>
+ <hasFiles>SPDXRef-JenaLib</hasFiles>
+ <hasFiles>SPDXRef-DoapSource</hasFiles>
+ <homepage>http://ftp.gnu.org/gnu/glibc</homepage>
+ <licenseComments>The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.</licenseComments>
+ <licenseConcluded>(LGPL-2.0-only OR LicenseRef-3)</licenseConcluded>
+ <licenseDeclared>(LGPL-2.0-only AND LicenseRef-3)</licenseDeclared>
+ <licenseInfoFromFiles>GPL-2.0-only</licenseInfoFromFiles>
+ <licenseInfoFromFiles>LicenseRef-2</licenseInfoFromFiles>
+ <licenseInfoFromFiles>LicenseRef-1</licenseInfoFromFiles>
+ <name>glibc</name>
+ <originator>Organization: ExampleCodeInspect (contact@example.com)</originator>
+ <packageFileName>glibc-2.11.1.tar.gz</packageFileName>
+ <packageVerificationCode>
+ <packageVerificationCodeExcludedFiles>./package.spdx</packageVerificationCodeExcludedFiles>
+ <packageVerificationCodeValue>d6a770ba38583ed4bb4525bd96e50461655d2758</packageVerificationCodeValue>
+ </packageVerificationCode>
+ <sourceInfo>uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.</sourceInfo>
+ <summary>GNU C library.</summary>
+ <supplier>Person: Jane Doe (jane.doe@example.com)</supplier>
+ <versionInfo>2.11.1</versionInfo>
+ </packages>
+ <packages>
+ <SPDXID>SPDXRef-fromDoap-1</SPDXID>
+ <copyrightText>NOASSERTION</copyrightText>
+ <downloadLocation>NOASSERTION</downloadLocation>
+ <filesAnalyzed>false</filesAnalyzed>
+ <homepage>http://commons.apache.org/proper/commons-lang/</homepage>
+ <licenseConcluded>NOASSERTION</licenseConcluded>
+ <licenseDeclared>NOASSERTION</licenseDeclared>
+ <name>Apache Commons Lang</name>
+ </packages>
+ <packages>
+ <SPDXID>SPDXRef-fromDoap-0</SPDXID>
+ <copyrightText>NOASSERTION</copyrightText>
+ <downloadLocation>https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz</downloadLocation>
+ <externalRefs>
+ <referenceCategory>PACKAGE_MANAGER</referenceCategory>
+ <referenceLocator>pkg:maven/org.apache.jena/apache-jena@3.12.0</referenceLocator>
+ <referenceType>purl</referenceType>
+ </externalRefs>
+ <filesAnalyzed>false</filesAnalyzed>
+ <homepage>http://www.openjena.org/</homepage>
+ <licenseConcluded>NOASSERTION</licenseConcluded>
+ <licenseDeclared>NOASSERTION</licenseDeclared>
+ <name>Jena</name>
+ <versionInfo>3.12.0</versionInfo>
+ </packages>
+ <packages>
+ <SPDXID>SPDXRef-Saxon</SPDXID>
+ <checksums>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>85ed0817af83a24ad8da68c2b5094de69833983c</checksumValue>
+ </checksums>
+ <copyrightText>Copyright Saxonica Ltd</copyrightText>
+ <description>The Saxon package is a collection of tools for processing XML documents.</description>
+ <downloadLocation>https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download</downloadLocation>
+ <filesAnalyzed>false</filesAnalyzed>
+ <homepage>http://saxon.sourceforge.net/</homepage>
+ <licenseComments>Other versions available for a commercial license</licenseComments>
+ <licenseConcluded>MPL-1.0</licenseConcluded>
+ <licenseDeclared>MPL-1.0</licenseDeclared>
+ <name>Saxon</name>
+ <packageFileName>saxonB-8.8.zip</packageFileName>
+ <versionInfo>8.8</versionInfo>
+ </packages>
+ <files>
+ <SPDXID>SPDXRef-DoapSource</SPDXID>
+ <checksums>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</checksumValue>
+ </checksums>
+ <copyrightText>Copyright 2010, 2011 Source Auditor Inc.</copyrightText>
+ <fileContributors>Protecode Inc.</fileContributors>
+ <fileContributors>SPDX Technical Team Members</fileContributors>
+ <fileContributors>Open Logic Inc.</fileContributors>
+ <fileContributors>Source Auditor Inc.</fileContributors>
+ <fileContributors>Black Duck Software In.c</fileContributors>
+ <fileName>./src/org/spdx/parser/DOAPProject.java</fileName>
+ <fileTypes>SOURCE</fileTypes>
+ <licenseConcluded>Apache-2.0</licenseConcluded>
+ <licenseInfoInFiles>Apache-2.0</licenseInfoInFiles>
+ </files>
+ <files>
+ <SPDXID>SPDXRef-CommonsLangSrc</SPDXID>
+ <checksums>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>c2b4e1c67a2d28fced849ee1bb76e7391b93f125</checksumValue>
+ </checksums>
+ <comment>This file is used by Jena</comment>
+ <copyrightText>Copyright 2001-2011 The Apache Software Foundation</copyrightText>
+ <fileContributors>Apache Software Foundation</fileContributors>
+ <fileName>./lib-source/commons-lang3-3.1-sources.jar</fileName>
+ <fileTypes>ARCHIVE</fileTypes>
+ <licenseConcluded>Apache-2.0</licenseConcluded>
+ <licenseInfoInFiles>Apache-2.0</licenseInfoInFiles>
+ <noticeText>Apache Commons Lang
+Copyright 2001-2011 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software from the Spring Framework,
+under the Apache License 2.0 (see: StringUtils.containsWhitespace())</noticeText>
+ </files>
+ <files>
+ <SPDXID>SPDXRef-JenaLib</SPDXID>
+ <checksums>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>3ab4e1c67a2d28fced849ee1bb76e7391b93f125</checksumValue>
+ </checksums>
+ <comment>This file belongs to Jena</comment>
+ <copyrightText>(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP</copyrightText>
+ <fileContributors>Apache Software Foundation</fileContributors>
+ <fileContributors>Hewlett Packard Inc.</fileContributors>
+ <fileName>./lib-source/jena-2.6.3-sources.jar</fileName>
+ <fileTypes>ARCHIVE</fileTypes>
+ <licenseComments>This license is used by Jena</licenseComments>
+ <licenseConcluded>LicenseRef-1</licenseConcluded>
+ <licenseInfoInFiles>LicenseRef-1</licenseInfoInFiles>
+ </files>
+ <files>
+ <SPDXID>SPDXRef-File</SPDXID>
+ <annotations>
+ <annotationDate>2011-01-29T18:30:22Z</annotationDate>
+ <annotationType>OTHER</annotationType>
+ <annotator>Person: File Commenter</annotator>
+ <comment>File level annotation</comment>
+ </annotations>
+ <checksums>
+ <algorithm>SHA1</algorithm>
+ <checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2758</checksumValue>
+ </checksums>
+ <checksums>
+ <algorithm>MD5</algorithm>
+ <checksumValue>624c1abb3664f4b35547e7c73864ad24</checksumValue>
+ </checksums>
+ <comment>The concluded license was taken from the package level that the file was included in.
+This information was found in the COPYING.txt file in the xyz directory.</comment>
+ <copyrightText>Copyright 2008-2010 John Smith</copyrightText>
+ <fileContributors>The Regents of the University of California</fileContributors>
+ <fileContributors>Modified by Paul Mundt lethal@linux-sh.org</fileContributors>
+ <fileContributors>IBM Corporation</fileContributors>
+ <fileName>./package/foo.c</fileName>
+ <fileTypes>SOURCE</fileTypes>
+ <licenseComments>The concluded license was taken from the package level that the file was included in.</licenseComments>
+ <licenseConcluded>(LGPL-2.0-only OR LicenseRef-2)</licenseConcluded>
+ <licenseInfoInFiles>GPL-2.0-only</licenseInfoInFiles>
+ <licenseInfoInFiles>LicenseRef-2</licenseInfoInFiles>
+ <noticeText>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.</noticeText>
+ </files>
+ <snippets>
+ <SPDXID>SPDXRef-Snippet</SPDXID>
+ <comment>This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.</comment>
+ <copyrightText>Copyright 2008-2010 John Smith</copyrightText>
+ <licenseComments>The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.</licenseComments>
+ <licenseConcluded>GPL-2.0-only</licenseConcluded>
+ <licenseInfoInSnippets>GPL-2.0-only</licenseInfoInSnippets>
+ <name>from linux kernel</name>
+ <ranges>
+ <endPointer>
+ <offset>420</offset>
+ <reference>SPDXRef-DoapSource</reference>
+ </endPointer>
+ <startPointer>
+ <offset>310</offset>
+ <reference>SPDXRef-DoapSource</reference>
+ </startPointer>
+ </ranges>
+ <ranges>
+ <endPointer>
+ <lineNumber>23</lineNumber>
+ <reference>SPDXRef-DoapSource</reference>
+ </endPointer>
+ <startPointer>
+ <lineNumber>5</lineNumber>
+ <reference>SPDXRef-DoapSource</reference>
+ </startPointer>
+ </ranges>
+ <snippetFromFile>SPDXRef-DoapSource</snippetFromFile>
+ </snippets>
+ <relationships>
+ <spdxElementId>SPDXRef-DOCUMENT</spdxElementId>
+ <relatedSpdxElement>SPDXRef-Package</relatedSpdxElement>
+ <relationshipType>CONTAINS</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-DOCUMENT</spdxElementId>
+ <relatedSpdxElement>DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement</relatedSpdxElement>
+ <relationshipType>COPY_OF</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-DOCUMENT</spdxElementId>
+ <relatedSpdxElement>SPDXRef-File</relatedSpdxElement>
+ <relationshipType>DESCRIBES</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-DOCUMENT</spdxElementId>
+ <relatedSpdxElement>SPDXRef-Package</relatedSpdxElement>
+ <relationshipType>DESCRIBES</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-Package</spdxElementId>
+ <relatedSpdxElement>SPDXRef-JenaLib</relatedSpdxElement>
+ <relationshipType>CONTAINS</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-Package</spdxElementId>
+ <relatedSpdxElement>SPDXRef-Saxon</relatedSpdxElement>
+ <relationshipType>DYNAMIC_LINK</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-CommonsLangSrc</spdxElementId>
+ <relatedSpdxElement>NOASSERTION</relatedSpdxElement>
+ <relationshipType>GENERATED_FROM</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-JenaLib</spdxElementId>
+ <relatedSpdxElement>SPDXRef-Package</relatedSpdxElement>
+ <relationshipType>CONTAINS</relationshipType>
+ </relationships>
+ <relationships>
+ <spdxElementId>SPDXRef-File</spdxElementId>
+ <relatedSpdxElement>SPDXRef-fromDoap-0</relatedSpdxElement>
+ <relationshipType>GENERATED_FROM</relationshipType>
+ </relationships>
+</Document>
diff --git a/examples/sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml b/examples/sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml
new file mode 100644
index 0000000..d58cf22
--- /dev/null
+++ b/examples/sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml
@@ -0,0 +1,390 @@
+---
+SPDXID: "SPDXRef-DOCUMENT"
+spdxVersion: "SPDX-2.2"
+creationInfo:
+ comment: "This package has been shipped in source and binary form.\nThe binaries\
+ \ were created with gcc 4.5.1 and expect to link to\ncompatible system run time\
+ \ libraries."
+ created: "2010-01-29T18:30:22Z"
+ creators:
+ - "Tool: LicenseFind-1.0"
+ - "Organization: ExampleCodeInspect ()"
+ - "Person: Jane Doe ()"
+ licenseListVersion: "3.9"
+name: "SPDX-Tools-v2.0"
+dataLicense: "CC0-1.0"
+comment: "This document was created using SPDX 2.0 using licenses from the web site."
+externalDocumentRefs:
+- externalDocumentId: "DocumentRef-spdx-tool-1.2"
+ checksum:
+ algorithm: "SHA1"
+ checksumValue: "d6a770ba38583ed4bb4525bd96e50461655d2759"
+ spdxDocument: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+hasExtractedLicensingInfos:
+- licenseId: "LicenseRef-1"
+ extractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007,\
+ \ 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n\
+ \ *\n * Redistribution and use in source and binary forms, with or without\n *\
+ \ modification, are permitted provided that the following conditions\n * are met:\n\
+ \ * 1. Redistributions of source code must retain the above copyright\n * notice,\
+ \ this list of conditions and the following disclaimer.\n * 2. Redistributions\
+ \ in binary form must reproduce the above copyright\n * notice, this list of\
+ \ conditions and the following disclaimer in the\n * documentation and/or other\
+ \ materials provided with the distribution.\n * 3. The name of the author may\
+ \ not be used to endorse or promote products\n * derived from this software\
+ \ without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED\
+ \ BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING,\
+ \ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS\
+ \ FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE\
+ \ LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\
+ \ DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\
+ \ OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\
+ \ CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\
+ \ OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\
+ \ USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\
+ */"
+- licenseId: "LicenseRef-2"
+ extractedText: "This package includes the GRDDL parser developed by Hewlett Packard\
+ \ under the following license:\n� Copyright 2007 Hewlett-Packard Development Company,\
+ \ LP\n\nRedistribution and use in source and binary forms, with or without modification,\
+ \ are permitted provided that the following conditions are met: \n\nRedistributions\
+ \ of source code must retain the above copyright notice, this list of conditions\
+ \ and the following disclaimer. \nRedistributions in binary form must reproduce\
+ \ the above copyright notice, this list of conditions and the following disclaimer\
+ \ in the documentation and/or other materials provided with the distribution.\
+ \ \nThe name of the author may not be used to endorse or promote products derived\
+ \ from this software without specific prior written permission. \nTHIS SOFTWARE\
+ \ IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,\
+ \ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\
+ \ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE\
+ \ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\
+ \ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\
+ \ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\
+ \ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\
+ \ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\
+ \ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+- licenseId: "LicenseRef-4"
+ extractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n\
+ \ *\n * Redistribution and use in source and binary forms, with or without\n *\
+ \ modification, are permitted provided that the following conditions\n * are met:\n\
+ \ * 1. Redistributions of source code must retain the above copyright\n * notice,\
+ \ this list of conditions and the following disclaimer.\n * 2. Redistributions\
+ \ in binary form must reproduce the above copyright\n * notice, this list of\
+ \ conditions and the following disclaimer in the\n * documentation and/or other\
+ \ materials provided with the distribution.\n * 3. The name of the author may\
+ \ not be used to endorse or promote products\n * derived from this software\
+ \ without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED\
+ \ BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING,\
+ \ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS\
+ \ FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE\
+ \ LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\
+ \ DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\
+ \ OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\
+ \ CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\
+ \ OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE\
+ \ USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\
+ */"
+- licenseId: "LicenseRef-Beerware-4.2"
+ comment: "The beerware license has a couple of other standard variants."
+ extractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote\
+ \ this file. As long as you retain this notice you\ncan do whatever you want with\
+ \ this stuff. If we meet some day, and you think this stuff is worth it, you can\
+ \ buy me a beer in return Poul-Henning Kamp"
+ name: "Beer-Ware License (Version 42)"
+ seeAlsos:
+ - "http://people.freebsd.org/~phk/"
+- licenseId: "LicenseRef-3"
+ comment: "This is tye CyperNeko License"
+ extractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright\
+ \ 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source\
+ \ and binary forms, with or without\nmodification, are permitted provided that\
+ \ the following conditions\nare met:\n\n1. Redistributions of source code must\
+ \ retain the above copyright\n notice, this list of conditions and the following\
+ \ disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n\
+ \ notice, this list of conditions and the following disclaimer in\n the documentation\
+ \ and/or other materials provided with the\n distribution.\n\n3. The end-user\
+ \ documentation included with the redistribution,\n if any, must include the\
+ \ following acknowledgment: \n \"This product includes software developed\
+ \ by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software\
+ \ itself,\n if and wherever such third-party acknowledgments normally appear.\n\
+ \n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n \
+ \ or promote products derived from this software without prior \n written permission.\
+ \ For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products\
+ \ derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\"\
+ \ appear in their name, without prior written\n permission of the author.\n\n\
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES,\
+ \ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND\
+ \ FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR\
+ \ OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\
+ \ EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT\
+ \ \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS\
+ \ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT,\
+ \ STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY\
+ \ WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF\
+ \ SUCH DAMAGE."
+ name: "CyberNeko License"
+ seeAlsos:
+ - "http://people.apache.org/~andyc/neko/LICENSE"
+ - "http://justasample.url.com"
+annotations:
+- annotationDate: "2010-01-29T18:30:22Z"
+ annotationType: "OTHER"
+ annotator: "Person: Jane Doe ()"
+ comment: "Document level annotation"
+- annotationDate: "2010-02-10T00:00:00Z"
+ annotationType: "REVIEW"
+ annotator: "Person: Joe Reviewer"
+ comment: "This is just an example. Some of the non-standard licenses look like\
+ \ they are actually BSD 3 clause licenses"
+- annotationDate: "2011-03-13T00:00:00Z"
+ annotationType: "REVIEW"
+ annotator: "Person: Suzanne Reviewer"
+ comment: "Another example reviewer."
+documentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301"
+documentDescribes:
+- "SPDXRef-File"
+- "SPDXRef-Package"
+packages:
+- SPDXID: "SPDXRef-Package"
+ annotations:
+ - annotationDate: "2011-01-29T18:30:22Z"
+ annotationType: "OTHER"
+ annotator: "Person: Package Commenter"
+ comment: "Package level annotation"
+ attributionTexts:
+ - "The GNU C Library is free software. See the file COPYING.LIB for copying conditions,\
+ \ and LICENSES for notices about a few contributions that require these additional\
+ \ notices to be distributed. License copyright years may be listed using range\
+ \ notation, e.g., 1996-2015, indicating that every year in the range, inclusive,\
+ \ is a copyrightable year that would otherwise be listed individually."
+ checksums:
+ - algorithm: "MD5"
+ checksumValue: "624c1abb3664f4b35547e7c73864ad24"
+ - algorithm: "SHA1"
+ checksumValue: "85ed0817af83a24ad8da68c2b5094de69833983c"
+ - algorithm: "SHA256"
+ checksumValue: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+ copyrightText: "Copyright 2008-2010 John Smith"
+ description: "The GNU C Library defines functions that are specified by the ISO\
+ \ C standard, as well as additional features specific to POSIX and other derivatives\
+ \ of the Unix operating system, and extensions specific to GNU systems."
+ downloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz"
+ externalRefs:
+ - referenceCategory: "SECURITY"
+ referenceLocator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+ referenceType: "cpe23Type"
+ - comment: "This is the external ref for Acme"
+ referenceCategory: "OTHER"
+ referenceLocator: "acmecorp/acmenator/4.1.3-alpha"
+ referenceType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge"
+ filesAnalyzed: true
+ hasFiles:
+ - "SPDXRef-CommonsLangSrc"
+ - "SPDXRef-JenaLib"
+ - "SPDXRef-DoapSource"
+ homepage: "http://ftp.gnu.org/gnu/glibc"
+ licenseComments: "The license for this project changed with the release of version\
+ \ x.y. The version of the project included here post-dates the license change."
+ licenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)"
+ licenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)"
+ licenseInfoFromFiles:
+ - "GPL-2.0-only"
+ - "LicenseRef-2"
+ - "LicenseRef-1"
+ name: "glibc"
+ originator: "Organization: ExampleCodeInspect (contact@example.com)"
+ packageFileName: "glibc-2.11.1.tar.gz"
+ packageVerificationCode:
+ packageVerificationCodeExcludedFiles:
+ - "./package.spdx"
+ packageVerificationCodeValue: "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ sourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git."
+ summary: "GNU C library."
+ supplier: "Person: Jane Doe (jane.doe@example.com)"
+ versionInfo: "2.11.1"
+- SPDXID: "SPDXRef-fromDoap-1"
+ copyrightText: "NOASSERTION"
+ downloadLocation: "NOASSERTION"
+ filesAnalyzed: false
+ homepage: "http://commons.apache.org/proper/commons-lang/"
+ licenseConcluded: "NOASSERTION"
+ licenseDeclared: "NOASSERTION"
+ name: "Apache Commons Lang"
+- SPDXID: "SPDXRef-fromDoap-0"
+ copyrightText: "NOASSERTION"
+ downloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz"
+ externalRefs:
+ - referenceCategory: "PACKAGE_MANAGER"
+ referenceLocator: "pkg:maven/org.apache.jena/apache-jena@3.12.0"
+ referenceType: "purl"
+ filesAnalyzed: false
+ homepage: "http://www.openjena.org/"
+ licenseConcluded: "NOASSERTION"
+ licenseDeclared: "NOASSERTION"
+ name: "Jena"
+ versionInfo: "3.12.0"
+- SPDXID: "SPDXRef-Saxon"
+ checksums:
+ - algorithm: "SHA1"
+ checksumValue: "85ed0817af83a24ad8da68c2b5094de69833983c"
+ copyrightText: "Copyright Saxonica Ltd"
+ description: "The Saxon package is a collection of tools for processing XML documents."
+ downloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download"
+ filesAnalyzed: false
+ homepage: "http://saxon.sourceforge.net/"
+ licenseComments: "Other versions available for a commercial license"
+ licenseConcluded: "MPL-1.0"
+ licenseDeclared: "MPL-1.0"
+ name: "Saxon"
+ packageFileName: "saxonB-8.8.zip"
+ versionInfo: "8.8"
+files:
+- SPDXID: "SPDXRef-DoapSource"
+ checksums:
+ - algorithm: "SHA1"
+ checksumValue: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ copyrightText: "Copyright 2010, 2011 Source Auditor Inc."
+ fileContributors:
+ - "Protecode Inc."
+ - "SPDX Technical Team Members"
+ - "Open Logic Inc."
+ - "Source Auditor Inc."
+ - "Black Duck Software In.c"
+ fileName: "./src/org/spdx/parser/DOAPProject.java"
+ fileTypes:
+ - "SOURCE"
+ licenseConcluded: "Apache-2.0"
+ licenseInfoInFiles:
+ - "Apache-2.0"
+- SPDXID: "SPDXRef-CommonsLangSrc"
+ checksums:
+ - algorithm: "SHA1"
+ checksumValue: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125"
+ comment: "This file is used by Jena"
+ copyrightText: "Copyright 2001-2011 The Apache Software Foundation"
+ fileContributors:
+ - "Apache Software Foundation"
+ fileName: "./lib-source/commons-lang3-3.1-sources.jar"
+ fileTypes:
+ - "ARCHIVE"
+ licenseConcluded: "Apache-2.0"
+ licenseInfoInFiles:
+ - "Apache-2.0"
+ noticeText: "Apache Commons Lang\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())"
+- SPDXID: "SPDXRef-JenaLib"
+ checksums:
+ - algorithm: "SHA1"
+ checksumValue: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125"
+ comment: "This file belongs to Jena"
+ copyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008,\
+ \ 2009 Hewlett-Packard Development Company, LP"
+ fileContributors:
+ - "Apache Software Foundation"
+ - "Hewlett Packard Inc."
+ fileName: "./lib-source/jena-2.6.3-sources.jar"
+ fileTypes:
+ - "ARCHIVE"
+ licenseComments: "This license is used by Jena"
+ licenseConcluded: "LicenseRef-1"
+ licenseInfoInFiles:
+ - "LicenseRef-1"
+- SPDXID: "SPDXRef-File"
+ annotations:
+ - annotationDate: "2011-01-29T18:30:22Z"
+ annotationType: "OTHER"
+ annotator: "Person: File Commenter"
+ comment: "File level annotation"
+ checksums:
+ - algorithm: "SHA1"
+ checksumValue: "d6a770ba38583ed4bb4525bd96e50461655d2758"
+ - algorithm: "MD5"
+ checksumValue: "624c1abb3664f4b35547e7c73864ad24"
+ comment: "The concluded license was taken from the package level that the file was\
+ \ included in.\nThis information was found in the COPYING.txt file in the xyz\
+ \ directory."
+ copyrightText: "Copyright 2008-2010 John Smith"
+ fileContributors:
+ - "The Regents of the University of California"
+ - "Modified by Paul Mundt lethal@linux-sh.org"
+ - "IBM Corporation"
+ fileName: "./package/foo.c"
+ fileTypes:
+ - "SOURCE"
+ licenseComments: "The concluded license was taken from the package level that the\
+ \ file was included in."
+ licenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)"
+ licenseInfoInFiles:
+ - "GPL-2.0-only"
+ - "LicenseRef-2"
+ noticeText: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is\
+ \ hereby granted, free of charge, to any person obtaining a copy of this software\
+ \ and associated documentation files (the �Software�), to deal in the Software\
+ \ without restriction, including without limitation the rights to use, copy, modify,\
+ \ merge, publish, distribute, sublicense, and/or sell copies of the Software,\
+ \ and to permit persons to whom the Software is furnished to do so, subject to\
+ \ the following conditions: \nThe above copyright notice and this permission notice\
+ \ shall be included in all copies or substantial portions of the Software.\n\n\
+ 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."
+snippets:
+- SPDXID: "SPDXRef-Snippet"
+ comment: "This snippet was identified as significant and highlighted in this Apache-2.0\
+ \ file, when a commercial scanner identified it as being derived from file foo.c\
+ \ in package xyz which is licensed under GPL-2.0."
+ copyrightText: "Copyright 2008-2010 John Smith"
+ licenseComments: "The concluded license was taken from package xyz, from which the\
+ \ snippet was copied into the current file. The concluded license information\
+ \ was found in the COPYING.txt file in package xyz."
+ licenseConcluded: "GPL-2.0-only"
+ licenseInfoInSnippets:
+ - "GPL-2.0-only"
+ name: "from linux kernel"
+ ranges:
+ - endPointer:
+ offset: 420
+ reference: "SPDXRef-DoapSource"
+ startPointer:
+ offset: 310
+ reference: "SPDXRef-DoapSource"
+ - endPointer:
+ lineNumber: 23
+ reference: "SPDXRef-DoapSource"
+ startPointer:
+ lineNumber: 5
+ reference: "SPDXRef-DoapSource"
+ snippetFromFile: "SPDXRef-DoapSource"
+relationships:
+- spdxElementId: "SPDXRef-DOCUMENT"
+ relatedSpdxElement: "SPDXRef-Package"
+ relationshipType: "CONTAINS"
+- spdxElementId: "SPDXRef-DOCUMENT"
+ relatedSpdxElement: "DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement"
+ relationshipType: "COPY_OF"
+- spdxElementId: "SPDXRef-DOCUMENT"
+ relatedSpdxElement: "SPDXRef-File"
+ relationshipType: "DESCRIBES"
+- spdxElementId: "SPDXRef-DOCUMENT"
+ relatedSpdxElement: "SPDXRef-Package"
+ relationshipType: "DESCRIBES"
+- spdxElementId: "SPDXRef-Package"
+ relatedSpdxElement: "SPDXRef-JenaLib"
+ relationshipType: "CONTAINS"
+- spdxElementId: "SPDXRef-Package"
+ relatedSpdxElement: "SPDXRef-Saxon"
+ relationshipType: "DYNAMIC_LINK"
+- spdxElementId: "SPDXRef-CommonsLangSrc"
+ relatedSpdxElement: "NOASSERTION"
+ relationshipType: "GENERATED_FROM"
+- spdxElementId: "SPDXRef-JenaLib"
+ relatedSpdxElement: "SPDXRef-Package"
+ relationshipType: "CONTAINS"
+- spdxElementId: "SPDXRef-File"
+ relatedSpdxElement: "SPDXRef-fromDoap-0"
+ relationshipType: "GENERATED_FROM"
diff --git a/examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml b/examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml
new file mode 100644
index 0000000..611b74c
--- /dev/null
+++ b/examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml
@@ -0,0 +1,412 @@
+SPDXID: SPDXRef-DOCUMENT
+annotations:
+- annotationDate: "2010-01-29T18:30:22Z"
+ annotationType: OTHER
+ annotator: 'Person: Jane Doe ()'
+ comment: Document level annotation
+- annotationDate: "2010-02-10T00:00:00Z"
+ annotationType: REVIEW
+ annotator: 'Person: Joe Reviewer'
+ comment: This is just an example. Some of the non-standard licenses look like they
+ are actually BSD 3 clause licenses
+- annotationDate: "2011-03-13T00:00:00Z"
+ annotationType: REVIEW
+ annotator: 'Person: Suzanne Reviewer'
+ comment: Another example reviewer.
+comment: This document was created using SPDX 2.0 using licenses from the web site.
+creationInfo:
+ comment: |-
+ This package has been shipped in source and binary form.
+ The binaries were created with gcc 4.5.1 and expect to link to
+ compatible system run time libraries.
+ created: "2010-01-29T18:30:22Z"
+ creators:
+ - 'Tool: LicenseFind-1.0'
+ - 'Organization: ExampleCodeInspect ()'
+ - 'Person: Jane Doe ()'
+ licenseListVersion: "3.9"
+dataLicense: CC0-1.0
+documentNamespace: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
+externalDocumentRefs:
+- checksum:
+ algorithm: SHA1
+ checksumValue: d6a770ba38583ed4bb4525bd96e50461655d2759
+ externalDocumentId: DocumentRef-spdx-tool-1.2
+ spdxDocument: http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301
+files:
+- SPDXID: SPDXRef-DoapSource
+ checksums:
+ - algorithm: SHA1
+ checksumValue: 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
+ copyrightText: Copyright 2010, 2011 Source Auditor Inc.
+ fileContributors:
+ - Protecode Inc.
+ - SPDX Technical Team Members
+ - Open Logic Inc.
+ - Source Auditor Inc.
+ - Black Duck Software In.c
+ fileName: ./src/org/spdx/parser/DOAPProject.java
+ fileTypes:
+ - SOURCE
+ licenseConcluded: Apache-2.0
+ licenseInfoInFiles:
+ - Apache-2.0
+- SPDXID: SPDXRef-CommonsLangSrc
+ checksums:
+ - algorithm: SHA1
+ checksumValue: c2b4e1c67a2d28fced849ee1bb76e7391b93f125
+ comment: This file is used by Jena
+ copyrightText: Copyright 2001-2011 The Apache Software Foundation
+ fileContributors:
+ - Apache Software Foundation
+ fileName: ./lib-source/commons-lang3-3.1-sources.jar
+ fileTypes:
+ - ARCHIVE
+ licenseConcluded: Apache-2.0
+ licenseInfoInFiles:
+ - Apache-2.0
+ noticeText: |-
+ Apache Commons Lang
+ Copyright 2001-2011 The Apache Software Foundation
+
+ This product includes software developed by
+ The Apache Software Foundation (http://www.apache.org/).
+
+ This product includes software from the Spring Framework,
+ under the Apache License 2.0 (see: StringUtils.containsWhitespace())
+- SPDXID: SPDXRef-JenaLib
+ checksums:
+ - algorithm: SHA1
+ checksumValue: 3ab4e1c67a2d28fced849ee1bb76e7391b93f125
+ comment: This file belongs to Jena
+ copyrightText: (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008,
+ 2009 Hewlett-Packard Development Company, LP
+ fileContributors:
+ - Apache Software Foundation
+ - Hewlett Packard Inc.
+ fileName: ./lib-source/jena-2.6.3-sources.jar
+ fileTypes:
+ - ARCHIVE
+ licenseComments: This license is used by Jena
+ licenseConcluded: LicenseRef-1
+ licenseInfoInFiles:
+ - LicenseRef-1
+- SPDXID: SPDXRef-File
+ annotations:
+ - annotationDate: "2011-01-29T18:30:22Z"
+ annotationType: OTHER
+ annotator: 'Person: File Commenter'
+ comment: File level annotation
+ checksums:
+ - algorithm: SHA1
+ checksumValue: d6a770ba38583ed4bb4525bd96e50461655d2758
+ - algorithm: MD5
+ checksumValue: 624c1abb3664f4b35547e7c73864ad24
+ comment: |-
+ The concluded license was taken from the package level that the file was included in.
+ This information was found in the COPYING.txt file in the xyz directory.
+ copyrightText: Copyright 2008-2010 John Smith
+ fileContributors:
+ - The Regents of the University of California
+ - Modified by Paul Mundt lethal@linux-sh.org
+ - IBM Corporation
+ fileName: ./package/foo.c
+ fileTypes:
+ - SOURCE
+ licenseComments: The concluded license was taken from the package level that the
+ file was included in.
+ licenseConcluded: (LGPL-2.0-only OR LicenseRef-2)
+ licenseInfoInFiles:
+ - GPL-2.0-only
+ - LicenseRef-2
+ noticeText: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is
+ hereby granted, free of charge, to any person obtaining a copy of this software
+ and associated documentation files (the �Software�), to deal in the Software without
+ restriction, including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+ persons to whom the Software is furnished to do so, subject to the following conditions:
+ \nThe above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS
+ IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ \ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE."
+hasExtractedLicensingInfos:
+- extractedText: |-
+ /*
+ * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ licenseId: LicenseRef-1
+- extractedText: "This package includes the GRDDL parser developed by Hewlett Packard
+ under the following license:\n� Copyright 2007 Hewlett-Packard Development Company,
+ LP\n\nRedistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met: \n\nRedistributions
+ of source code must retain the above copyright notice, this list of conditions
+ and the following disclaimer. \nRedistributions in binary form must reproduce
+ the above copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the distribution. \nThe
+ name of the author may not be used to endorse or promote products derived from
+ this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED
+ BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ OF THE POSSIBILITY OF SUCH DAMAGE."
+ licenseId: LicenseRef-2
+- extractedText: |-
+ /*
+ * (c) Copyright 2009 University of Bristol
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+ licenseId: LicenseRef-4
+- comment: The beerware license has a couple of other standard variants.
+ extractedText: |-
+ "THE BEER-WARE LICENSE" (Revision 42):
+ phk@FreeBSD.ORG wrote this file. As long as you retain this notice you
+ can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp
+ licenseId: LicenseRef-Beerware-4.2
+ name: Beer-Ware License (Version 42)
+ seeAlsos:
+ - http://people.freebsd.org/~phk/
+- comment: This is tye CyperNeko License
+ extractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright
+ 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source
+ and binary forms, with or without\nmodification, are permitted provided that the
+ following conditions\nare met:\n\n1. Redistributions of source code must retain
+ the above copyright\n notice, this list of conditions and the following disclaimer.
+ \n\n2. Redistributions in binary form must reproduce the above copyright\n notice,
+ this list of conditions and the following disclaimer in\n the documentation
+ and/or other materials provided with the\n distribution.\n\n3. The end-user
+ documentation included with the redistribution,\n if any, must include the following
+ acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n
+ \ Alternately, this acknowledgment may appear in the software itself,\n if
+ and wherever such third-party acknowledgments normally appear.\n\n4. The names
+ \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products
+ derived from this software without prior \n written permission. For written
+ permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from
+ this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear
+ in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE
+ IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT
+ NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ licenseId: LicenseRef-3
+ name: CyberNeko License
+ seeAlsos:
+ - http://people.apache.org/~andyc/neko/LICENSE
+ - http://justasample.url.com
+name: SPDX-Tools-v2.0
+packages:
+- SPDXID: SPDXRef-Package
+ annotations:
+ - annotationDate: "2011-01-29T18:30:22Z"
+ annotationType: OTHER
+ annotator: 'Person: Package Commenter'
+ comment: Package level annotation
+ attributionTexts:
+ - The GNU C Library is free software. See the file COPYING.LIB for copying conditions,
+ and LICENSES for notices about a few contributions that require these additional
+ notices to be distributed. License copyright years may be listed using range
+ notation, e.g., 1996-2015, indicating that every year in the range, inclusive,
+ is a copyrightable year that would otherwise be listed individually.
+ checksums:
+ - algorithm: MD5
+ checksumValue: 624c1abb3664f4b35547e7c73864ad24
+ - algorithm: SHA1
+ checksumValue: 85ed0817af83a24ad8da68c2b5094de69833983c
+ - algorithm: SHA256
+ checksumValue: 11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd
+ copyrightText: Copyright 2008-2010 John Smith
+ description: The GNU C Library defines functions that are specified by the ISO C
+ standard, as well as additional features specific to POSIX and other derivatives
+ of the Unix operating system, and extensions specific to GNU systems.
+ downloadLocation: http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz
+ externalRefs:
+ - comment: ""
+ referenceCategory: SECURITY
+ referenceLocator: cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*
+ referenceType: cpe23Type
+ - comment: This is the external ref for Acme
+ referenceCategory: OTHER
+ referenceLocator: acmecorp/acmenator/4.1.3-alpha
+ referenceType: http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge
+ filesAnalyzed: true
+ homepage: http://ftp.gnu.org/gnu/glibc
+ licenseComments: The license for this project changed with the release of version
+ x.y. The version of the project included here post-dates the license change.
+ licenseConcluded: (LGPL-2.0-only OR LicenseRef-3)
+ licenseDeclared: (LGPL-2.0-only AND LicenseRef-3)
+ licenseInfoFromFiles:
+ - GPL-2.0-only
+ - LicenseRef-2
+ - LicenseRef-1
+ name: glibc
+ originator: 'Organization: ExampleCodeInspect (contact@example.com)'
+ packageFileName: glibc-2.11.1.tar.gz
+ packageVerificationCode:
+ packageVerificationCodeExcludedFiles:
+ - ./package.spdx
+ packageVerificationCodeValue: d6a770ba38583ed4bb4525bd96e50461655d2758
+ sourceInfo: uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.
+ summary: GNU C library.
+ supplier: 'Person: Jane Doe (jane.doe@example.com)'
+ versionInfo: 2.11.1
+- SPDXID: SPDXRef-fromDoap-1
+ copyrightText: NOASSERTION
+ downloadLocation: NOASSERTION
+ homepage: http://commons.apache.org/proper/commons-lang/
+ licenseConcluded: NOASSERTION
+ licenseDeclared: NOASSERTION
+ name: Apache Commons Lang
+- SPDXID: SPDXRef-fromDoap-0
+ copyrightText: NOASSERTION
+ downloadLocation: https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz
+ externalRefs:
+ - comment: ""
+ referenceCategory: PACKAGE_MANAGER
+ referenceLocator: pkg:maven/org.apache.jena/apache-jena@3.12.0
+ referenceType: purl
+ homepage: http://www.openjena.org/
+ licenseConcluded: NOASSERTION
+ licenseDeclared: NOASSERTION
+ name: Jena
+ versionInfo: 3.12.0
+- SPDXID: SPDXRef-Saxon
+ checksums:
+ - algorithm: SHA1
+ checksumValue: 85ed0817af83a24ad8da68c2b5094de69833983c
+ copyrightText: Copyright Saxonica Ltd
+ description: The Saxon package is a collection of tools for processing XML documents.
+ downloadLocation: https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download
+ homepage: http://saxon.sourceforge.net/
+ licenseComments: Other versions available for a commercial license
+ licenseConcluded: MPL-1.0
+ licenseDeclared: MPL-1.0
+ name: Saxon
+ packageFileName: saxonB-8.8.zip
+ versionInfo: "8.8"
+- SPDXID: SPDXRef-CentOS-7
+ builtDate: "2021-09-15T02:38:00Z"
+ copyrightText: NOASSERTION
+ description: The CentOS container used to run the application.
+ downloadLocation: NOASSERTION
+ homepage: https://www.centos.org/
+ name: centos
+ packageFileName: saxonB-8.8.zip
+ primaryPackagePurpose: CONTAINER
+ releaseDate: "2021-10-15T02:38:00Z"
+ validUntilDate: "2022-10-15T02:38:00Z"
+ versionInfo: centos7.9.2009
+relationships:
+- comment: A relationship comment
+ relatedSpdxElement: SPDXRef-Package
+ relationshipType: CONTAINS
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement
+ relationshipType: COPY_OF
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: SPDXRef-File
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: SPDXRef-Package
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+- relatedSpdxElement: SPDXRef-JenaLib
+ relationshipType: CONTAINS
+ spdxElementId: SPDXRef-Package
+- relatedSpdxElement: SPDXRef-Saxon
+ relationshipType: DYNAMIC_LINK
+ spdxElementId: SPDXRef-Package
+- relatedSpdxElement: NOASSERTION
+ relationshipType: GENERATED_FROM
+ spdxElementId: SPDXRef-CommonsLangSrc
+- relatedSpdxElement: SPDXRef-Package
+ relationshipType: CONTAINS
+ spdxElementId: SPDXRef-JenaLib
+- relatedSpdxElement: SPDXRef-fromDoap-0
+ relationshipType: GENERATED_FROM
+ spdxElementId: SPDXRef-File
+snippets:
+- SPDXID: SPDXRef-Snippet
+ comment: This snippet was identified as significant and highlighted in this Apache-2.0
+ file, when a commercial scanner identified it as being derived from file foo.c
+ in package xyz which is licensed under GPL-2.0.
+ copyrightText: Copyright 2008-2010 John Smith
+ licenseComments: The concluded license was taken from package xyz, from which the
+ snippet was copied into the current file. The concluded license information was
+ found in the COPYING.txt file in package xyz.
+ licenseConcluded: GPL-2.0-only
+ licenseInfoInSnippets:
+ - GPL-2.0-only
+ name: from linux kernel
+ ranges:
+ - endPointer:
+ offset: 420
+ reference: SPDXRef-DoapSource
+ startPointer:
+ offset: 310
+ reference: SPDXRef-DoapSource
+ - endPointer:
+ lineNumber: 23
+ reference: SPDXRef-DoapSource
+ startPointer:
+ lineNumber: 5
+ reference: SPDXRef-DoapSource
+ snippetFromFile: SPDXRef-DoapSource
+spdxVersion: SPDX-2.2
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..1017b95
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,9 @@
+module github.com/spdx/tools-golang
+
+go 1.13
+
+require (
+ github.com/google/go-cmp v0.5.7
+ github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
+ sigs.k8s.io/yaml v1.3.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..85ffe1c
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,14 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb h1:bLo8hvc8XFm9J47r690TUKBzcjSWdJDxmjXJZ+/f92U=
+github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
+sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/idsearcher/idsearcher.go b/idsearcher/idsearcher.go
new file mode 100644
index 0000000..a5176ca
--- /dev/null
+++ b/idsearcher/idsearcher.go
@@ -0,0 +1,483 @@
+// Package idsearcher is used to search for short-form IDs in files
+// within a directory, and to build an SPDX Document containing those
+// license findings.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package idsearcher
+
+import (
+ "bufio"
+ "fmt"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/spdx/tools-golang/builder"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/utils"
+)
+
+// ===== 2.1 Searcher functions =====
+
+// Config2_1 is a collection of configuration settings for docbuilder
+// (for version 2.1 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_1.
+type Config2_1 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // BuilderPathsIgnored lists certain paths to be omitted from the built
+ // document. Each string should be a path, relative to the package's
+ // dirRoot, to a specific file or (for all files in a directory) ending
+ // in a slash. Prefix the string with "**" to omit all instances of that
+ // file / directory, regardless of where it is in the file tree.
+ BuilderPathsIgnored []string
+
+ // SearcherPathsIgnored lists certain paths that should not be searched
+ // by idsearcher, even if those paths have Files present. It uses the
+ // same format as BuilderPathsIgnored.
+ SearcherPathsIgnored []string
+}
+
+// BuildIDsDocument2_1 creates an SPDX Document (version 2.1) and searches for
+// short-form IDs in each file, filling in license fields as appropriate. It
+// returns that document or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - namespacePrefix: URI representing a prefix for the
+// namespace with which the SPDX Document will be associated
+func BuildIDsDocument2_1(packageName string, dirRoot string, idconfig *Config2_1) (*v2_1.Document, error) {
+ // first, build the Document using builder
+ bconfig := &builder.Config2_1{
+ NamespacePrefix: idconfig.NamespacePrefix,
+ CreatorType: "Tool",
+ Creator: "github.com/spdx/tools-golang/idsearcher",
+ PathsIgnored: idconfig.BuilderPathsIgnored,
+ }
+ doc, err := builder.Build2_1(packageName, dirRoot, bconfig)
+ if err != nil {
+ return nil, err
+ }
+ if doc == nil {
+ return nil, fmt.Errorf("builder returned nil Document")
+ }
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("builder returned nil Packages map")
+ }
+ if len(doc.Packages) != 1 {
+ return nil, fmt.Errorf("builder returned %d Packages", len(doc.Packages))
+ }
+
+ // now, walk through each file and find its licenses (if any)
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ return nil, fmt.Errorf("builder returned nil Package")
+ }
+ if pkg.Files == nil {
+ return nil, fmt.Errorf("builder returned nil Files in Package")
+ }
+ licsForPackage := map[string]int{}
+ for _, f := range pkg.Files {
+ // start by initializing / clearing values
+ f.LicenseInfoInFiles = []string{"NOASSERTION"}
+ f.LicenseConcluded = "NOASSERTION"
+
+ // check whether the searcher should ignore this file
+ if utils.ShouldIgnore(f.FileName, idconfig.SearcherPathsIgnored) {
+ continue
+ }
+
+ fPath := filepath.Join(dirRoot, f.FileName)
+ // FIXME this is not preferable -- ignoring error
+ ids, _ := searchFileIDs(fPath)
+ // FIXME for now, proceed onwards with whatever IDs we obtained.
+ // FIXME instead of ignoring the error, should probably either log it,
+ // FIXME and/or enable the caller to configure what should happen.
+
+ // separate out for this file's licenses
+ licsForFile := map[string]int{}
+ licsParens := []string{}
+ for _, lid := range ids {
+ // get individual elements and add for file and package
+ licElements := getIndividualLicenses(lid)
+ for _, elt := range licElements {
+ licsForFile[elt] = 1
+ licsForPackage[elt] = 1
+ }
+ // parenthesize if needed and add to slice for joining
+ licsParens = append(licsParens, makeElement(lid))
+ }
+
+ // OK -- now we can fill in the file's details, or NOASSERTION if none
+ if len(licsForFile) > 0 {
+ f.LicenseInfoInFiles = []string{}
+ for lic := range licsForFile {
+ f.LicenseInfoInFiles = append(f.LicenseInfoInFiles, lic)
+ }
+ sort.Strings(f.LicenseInfoInFiles)
+ // avoid adding parens and joining for single-ID items
+ if len(licsParens) == 1 {
+ f.LicenseConcluded = ids[0]
+ } else {
+ f.LicenseConcluded = strings.Join(licsParens, " AND ")
+ }
+ }
+ }
+
+ // and finally, we can fill in the package's details
+ if len(licsForPackage) == 0 {
+ pkg.PackageLicenseInfoFromFiles = []string{"NOASSERTION"}
+ } else {
+ pkg.PackageLicenseInfoFromFiles = []string{}
+ for lic := range licsForPackage {
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, lic)
+ }
+ sort.Strings(pkg.PackageLicenseInfoFromFiles)
+ }
+
+ return doc, nil
+}
+
+// ===== 2.2 Searcher functions =====
+
+// Config2_2 is a collection of configuration settings for docbuilder
+// (for version 2.2 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_2.
+type Config2_2 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // BuilderPathsIgnored lists certain paths to be omitted from the built
+ // document. Each string should be a path, relative to the package's
+ // dirRoot, to a specific file or (for all files in a directory) ending
+ // in a slash. Prefix the string with "**" to omit all instances of that
+ // file / directory, regardless of where it is in the file tree.
+ BuilderPathsIgnored []string
+
+ // SearcherPathsIgnored lists certain paths that should not be searched
+ // by idsearcher, even if those paths have Files present. It uses the
+ // same format as BuilderPathsIgnored.
+ SearcherPathsIgnored []string
+}
+
+// BuildIDsDocument2_2 creates an SPDX Document (version 2.2) and searches for
+// short-form IDs in each file, filling in license fields as appropriate. It
+// returns that document or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - namespacePrefix: URI representing a prefix for the
+// namespace with which the SPDX Document will be associated
+func BuildIDsDocument2_2(packageName string, dirRoot string, idconfig *Config2_2) (*v2_2.Document, error) {
+ // first, build the Document using builder
+ bconfig := &builder.Config2_2{
+ NamespacePrefix: idconfig.NamespacePrefix,
+ CreatorType: "Tool",
+ Creator: "github.com/spdx/tools-golang/idsearcher",
+ PathsIgnored: idconfig.BuilderPathsIgnored,
+ }
+ doc, err := builder.Build2_2(packageName, dirRoot, bconfig)
+ if err != nil {
+ return nil, err
+ }
+ if doc == nil {
+ return nil, fmt.Errorf("builder returned nil Document")
+ }
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("builder returned nil Packages map")
+ }
+ if len(doc.Packages) != 1 {
+ return nil, fmt.Errorf("builder returned %d Packages", len(doc.Packages))
+ }
+
+ // now, walk through each file and find its licenses (if any)
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ return nil, fmt.Errorf("builder returned nil Package")
+ }
+ if pkg.Files == nil {
+ return nil, fmt.Errorf("builder returned nil Files in Package")
+ }
+ licsForPackage := map[string]int{}
+ for _, f := range pkg.Files {
+ // start by initializing / clearing values
+ f.LicenseInfoInFiles = []string{"NOASSERTION"}
+ f.LicenseConcluded = "NOASSERTION"
+
+ // check whether the searcher should ignore this file
+ if utils.ShouldIgnore(f.FileName, idconfig.SearcherPathsIgnored) {
+ continue
+ }
+
+ fPath := filepath.Join(dirRoot, f.FileName)
+ // FIXME this is not preferable -- ignoring error
+ ids, _ := searchFileIDs(fPath)
+ // FIXME for now, proceed onwards with whatever IDs we obtained.
+ // FIXME instead of ignoring the error, should probably either log it,
+ // FIXME and/or enable the caller to configure what should happen.
+
+ // separate out for this file's licenses
+ licsForFile := map[string]int{}
+ licsParens := []string{}
+ for _, lid := range ids {
+ // get individual elements and add for file and package
+ licElements := getIndividualLicenses(lid)
+ for _, elt := range licElements {
+ licsForFile[elt] = 1
+ licsForPackage[elt] = 1
+ }
+ // parenthesize if needed and add to slice for joining
+ licsParens = append(licsParens, makeElement(lid))
+ }
+
+ // OK -- now we can fill in the file's details, or NOASSERTION if none
+ if len(licsForFile) > 0 {
+ f.LicenseInfoInFiles = []string{}
+ for lic := range licsForFile {
+ f.LicenseInfoInFiles = append(f.LicenseInfoInFiles, lic)
+ }
+ sort.Strings(f.LicenseInfoInFiles)
+ // avoid adding parens and joining for single-ID items
+ if len(licsParens) == 1 {
+ f.LicenseConcluded = ids[0]
+ } else {
+ f.LicenseConcluded = strings.Join(licsParens, " AND ")
+ }
+ }
+ }
+
+ // and finally, we can fill in the package's details
+ if len(licsForPackage) == 0 {
+ pkg.PackageLicenseInfoFromFiles = []string{"NOASSERTION"}
+ } else {
+ pkg.PackageLicenseInfoFromFiles = []string{}
+ for lic := range licsForPackage {
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, lic)
+ }
+ sort.Strings(pkg.PackageLicenseInfoFromFiles)
+ }
+
+ return doc, nil
+}
+
+// ===== 2.3 Searcher functions =====
+
+// Config2_3 is a collection of configuration settings for docbuilder
+// (for version 2.3 SPDX Documents). A few mandatory fields are set here
+// so that they can be repeatedly reused in multiple calls to Build2_3.
+type Config2_3 struct {
+ // NamespacePrefix should be a URI representing a prefix for the
+ // namespace with which the SPDX Document will be associated.
+ // It will be used in the DocumentNamespace field in the CreationInfo
+ // section, followed by the per-Document package name and a random UUID.
+ NamespacePrefix string
+
+ // BuilderPathsIgnored lists certain paths to be omitted from the built
+ // document. Each string should be a path, relative to the package's
+ // dirRoot, to a specific file or (for all files in a directory) ending
+ // in a slash. Prefix the string with "**" to omit all instances of that
+ // file / directory, regardless of where it is in the file tree.
+ BuilderPathsIgnored []string
+
+ // SearcherPathsIgnored lists certain paths that should not be searched
+ // by idsearcher, even if those paths have Files present. It uses the
+ // same format as BuilderPathsIgnored.
+ SearcherPathsIgnored []string
+}
+
+// BuildIDsDocument2_3 creates an SPDX Document (version 2.3) and searches for
+// short-form IDs in each file, filling in license fields as appropriate. It
+// returns that document or error if any is encountered. Arguments:
+// - packageName: name of package / directory
+// - dirRoot: path to directory to be analyzed
+// - namespacePrefix: URI representing a prefix for the
+// namespace with which the SPDX Document will be associated
+func BuildIDsDocument2_3(packageName string, dirRoot string, idconfig *Config2_3) (*v2_3.Document, error) {
+ // first, build the Document using builder
+ bconfig := &builder.Config2_3{
+ NamespacePrefix: idconfig.NamespacePrefix,
+ CreatorType: "Tool",
+ Creator: "github.com/spdx/tools-golang/idsearcher",
+ PathsIgnored: idconfig.BuilderPathsIgnored,
+ }
+ doc, err := builder.Build2_3(packageName, dirRoot, bconfig)
+ if err != nil {
+ return nil, err
+ }
+ if doc == nil {
+ return nil, fmt.Errorf("builder returned nil Document")
+ }
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("builder returned nil Packages map")
+ }
+ if len(doc.Packages) != 1 {
+ return nil, fmt.Errorf("builder returned %d Packages", len(doc.Packages))
+ }
+
+ // now, walk through each file and find its licenses (if any)
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ return nil, fmt.Errorf("builder returned nil Package")
+ }
+ if pkg.Files == nil {
+ return nil, fmt.Errorf("builder returned nil Files in Package")
+ }
+ licsForPackage := map[string]int{}
+ for _, f := range pkg.Files {
+ // start by initializing / clearing values
+ f.LicenseInfoInFiles = []string{"NOASSERTION"}
+ f.LicenseConcluded = "NOASSERTION"
+
+ // check whether the searcher should ignore this file
+ if utils.ShouldIgnore(f.FileName, idconfig.SearcherPathsIgnored) {
+ continue
+ }
+
+ fPath := filepath.Join(dirRoot, f.FileName)
+ // FIXME this is not preferable -- ignoring error
+ ids, _ := searchFileIDs(fPath)
+ // FIXME for now, proceed onwards with whatever IDs we obtained.
+ // FIXME instead of ignoring the error, should probably either log it,
+ // FIXME and/or enable the caller to configure what should happen.
+
+ // separate out for this file's licenses
+ licsForFile := map[string]int{}
+ licsParens := []string{}
+ for _, lid := range ids {
+ // get individual elements and add for file and package
+ licElements := getIndividualLicenses(lid)
+ for _, elt := range licElements {
+ licsForFile[elt] = 1
+ licsForPackage[elt] = 1
+ }
+ // parenthesize if needed and add to slice for joining
+ licsParens = append(licsParens, makeElement(lid))
+ }
+
+ // OK -- now we can fill in the file's details, or NOASSERTION if none
+ if len(licsForFile) > 0 {
+ f.LicenseInfoInFiles = []string{}
+ for lic := range licsForFile {
+ f.LicenseInfoInFiles = append(f.LicenseInfoInFiles, lic)
+ }
+ sort.Strings(f.LicenseInfoInFiles)
+ // avoid adding parens and joining for single-ID items
+ if len(licsParens) == 1 {
+ f.LicenseConcluded = ids[0]
+ } else {
+ f.LicenseConcluded = strings.Join(licsParens, " AND ")
+ }
+ }
+ }
+
+ // and finally, we can fill in the package's details
+ if len(licsForPackage) == 0 {
+ pkg.PackageLicenseInfoFromFiles = []string{"NOASSERTION"}
+ } else {
+ pkg.PackageLicenseInfoFromFiles = []string{}
+ for lic := range licsForPackage {
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, lic)
+ }
+ sort.Strings(pkg.PackageLicenseInfoFromFiles)
+ }
+
+ return doc, nil
+}
+
+// ===== Utility functions (not version-specific) =====
+func searchFileIDs(filePath string) ([]string, error) {
+ idsMap := map[string]int{}
+ ids := []string{}
+
+ f, err := os.Open(filePath)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+
+ for scanner.Scan() {
+ if strings.Contains(scanner.Text(), "SPDX-License-Identifier:") {
+ strs := strings.SplitN(scanner.Text(), "SPDX-License-Identifier:", 2)
+
+ // if prefixed by more than n characters, it's probably not a
+ // short-form ID; it's probably code to detect short-form IDs.
+ // Like this function itself, for example =)
+ prefix := stripTrash(strs[0])
+ if len(prefix) > 5 {
+ continue
+ }
+
+ // stop before trailing */ if it is present
+ lidToExtract := strs[1]
+ lidToExtract = strings.Split(lidToExtract, "*/")[0]
+ lid := strings.TrimSpace(lidToExtract)
+ lid = stripTrash(lid)
+ idsMap[lid] = 1
+ }
+ }
+
+ // FIXME for now, ignore scanner errors because we want to return whatever
+ // FIXME IDs were in fact found. should probably be changed to either
+ // FIXME log the error, and/or be configurable for what should happen.
+ // if err = scanner.Err(); err != nil {
+ // return nil, err
+ // }
+
+ // now, convert map to string
+ for lid := range idsMap {
+ ids = append(ids, lid)
+ }
+
+ // and sort it
+ sort.Strings(ids)
+
+ return ids, nil
+}
+
+func stripTrash(lid string) string {
+ re := regexp.MustCompile(`[^\w\s\d.\-\+()]+`)
+ return re.ReplaceAllString(lid, "")
+}
+
+func makeElement(lic string) string {
+ if strings.Contains(lic, " AND ") || strings.Contains(lic, " OR ") {
+ return fmt.Sprintf("(%s)", lic)
+ }
+
+ return lic
+}
+
+func getIndividualLicenses(lic string) []string {
+ // replace parens and '+' with spaces
+ lic = strings.Replace(lic, "(", " ", -1)
+ lic = strings.Replace(lic, ")", " ", -1)
+ lic = strings.Replace(lic, "+", " ", -1)
+
+ // now, split by spaces, trim, and add to slice
+ licElements := strings.Split(lic, " ")
+ lics := []string{}
+ for _, elt := range licElements {
+ elt := strings.TrimSpace(elt)
+ // don't add if empty or if case-insensitive operator
+ if elt == "" || strings.EqualFold(elt, "AND") ||
+ strings.EqualFold(elt, "OR") || strings.EqualFold(elt, "WITH") {
+ continue
+ }
+
+ lics = append(lics, elt)
+ }
+
+ // sort before returning
+ sort.Strings(lics)
+ return lics
+}
diff --git a/idsearcher/idsearcher_test.go b/idsearcher/idsearcher_test.go
new file mode 100644
index 0000000..7d7a5bb
--- /dev/null
+++ b/idsearcher/idsearcher_test.go
@@ -0,0 +1,1187 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package idsearcher
+
+import (
+ "testing"
+)
+
+// ===== 2.1 Searcher top-level function tests =====
+func Test2_1SearcherCanFillInIDs(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "../testdata/project2/"
+ config := &Config2_1{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ }
+
+ doc, err := BuildIDsDocument2_1(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking size of each
+ if doc.Packages == nil {
+ t.Fatalf("expected non-nil Packages, got nil")
+ }
+ if len(doc.Packages) != 1 {
+ t.Fatalf("expected Packages len to be 1, got %d", len(doc.Packages))
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil Files, got nil")
+ }
+ if len(pkg.Files) != 6 {
+ t.Fatalf("expected Files len to be 6, got %d", len(pkg.Files))
+ }
+
+ fileInFolder := pkg.Files[0]
+ if fileInFolder.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileInFolder.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileInFolder.LicenseInfoInFiles))
+ }
+ if fileInFolder.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseInfoInFiles[0])
+ }
+ if fileInFolder.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseConcluded)
+ }
+
+ fileTrailingComment := pkg.Files[1]
+ if fileTrailingComment.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileTrailingComment.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileTrailingComment.LicenseInfoInFiles))
+ }
+ if fileTrailingComment.LicenseInfoInFiles[0] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseInfoInFiles[0])
+ }
+ if fileTrailingComment.LicenseConcluded != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseConcluded)
+ }
+
+ fileHasDuplicateID := pkg.Files[2]
+ if fileHasDuplicateID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasDuplicateID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileHasDuplicateID.LicenseInfoInFiles))
+ }
+ if fileHasDuplicateID.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseInfoInFiles[0])
+ }
+ if fileHasDuplicateID.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseConcluded)
+ }
+
+ fileHasID := pkg.Files[3]
+ if fileHasID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasID.LicenseInfoInFiles) != 2 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 2, got %d", len(fileHasID.LicenseInfoInFiles))
+ }
+ if fileHasID.LicenseInfoInFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", fileHasID.LicenseInfoInFiles[0])
+ }
+ if fileHasID.LicenseInfoInFiles[1] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileHasID.LicenseInfoInFiles[1])
+ }
+ if fileHasID.LicenseConcluded != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "Apache-2.0 OR GPL-2.0-or-later", fileHasID.LicenseConcluded)
+ }
+
+ fileMultipleIDs := pkg.Files[4]
+ if fileMultipleIDs.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileMultipleIDs.LicenseInfoInFiles) != 5 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 5, got %d", len(fileMultipleIDs.LicenseInfoInFiles))
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[0] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", fileMultipleIDs.LicenseInfoInFiles[0])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[1] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", fileMultipleIDs.LicenseInfoInFiles[1])
+ }
+ // here, DO NOT keep the +
+ if fileMultipleIDs.LicenseInfoInFiles[2] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", fileMultipleIDs.LicenseInfoInFiles[2])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[3] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", fileMultipleIDs.LicenseInfoInFiles[3])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[4] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileMultipleIDs.LicenseInfoInFiles[4])
+ }
+ if fileMultipleIDs.LicenseConcluded != "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+" {
+ t.Errorf("expected %v, got %v", "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+", fileMultipleIDs.LicenseConcluded)
+ }
+
+ fileNoID := pkg.Files[5]
+ if fileNoID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileNoID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileNoID.LicenseInfoInFiles))
+ }
+ if fileNoID.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseInfoInFiles[0])
+ }
+ if fileNoID.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseConcluded)
+ }
+
+ // and finally, the package should have all of these licenses
+ if pkg.PackageLicenseInfoFromFiles == nil {
+ t.Fatalf("expected non-nil PackageLicenseInfoFromFiles, got nil")
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 7 {
+ t.Fatalf("expected PackageLicenseInfoFromFiles len to be 7, got %d", len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseInfoFromFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", pkg.PackageLicenseInfoFromFiles[0])
+ }
+ if pkg.PackageLicenseInfoFromFiles[1] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", pkg.PackageLicenseInfoFromFiles[1])
+ }
+ if pkg.PackageLicenseInfoFromFiles[2] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", pkg.PackageLicenseInfoFromFiles[2])
+ }
+ // here, DO NOT keep the +
+ if pkg.PackageLicenseInfoFromFiles[3] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", pkg.PackageLicenseInfoFromFiles[3])
+ }
+ if pkg.PackageLicenseInfoFromFiles[4] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", pkg.PackageLicenseInfoFromFiles[4])
+ }
+ if pkg.PackageLicenseInfoFromFiles[5] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", pkg.PackageLicenseInfoFromFiles[5])
+ }
+ if pkg.PackageLicenseInfoFromFiles[6] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", pkg.PackageLicenseInfoFromFiles[6])
+ }
+
+}
+
+func Test2_1SearcherCanFillInIDsAndIgnorePaths(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../testdata/project3/"
+ config := &Config2_1{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ BuilderPathsIgnored: []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ },
+ SearcherPathsIgnored: []string{
+ "**/dontscan.txt",
+ },
+ }
+
+ doc, err := BuildIDsDocument2_1(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking licenses for each, and
+ // confirming NOASSERTION for those that are skipped
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected len %d, got %d", 5, len(pkg.Files))
+ }
+
+ f := pkg.Files[0]
+ if f.FileName != "./dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[1]
+ if f.FileName != "./keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[2]
+ if f.FileName != "./keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[3]
+ if f.FileName != "./subdir/keep/dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[4]
+ if f.FileName != "./subdir/keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+}
+
+func Test2_1SearcherFailsWithInvalidPath(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "./oops/invalid"
+ config := &Config2_1{
+ NamespacePrefix: "whatever",
+ }
+
+ _, err := BuildIDsDocument2_1(packageName, dirRoot, config)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.2 Searcher top-level function tests =====
+func Test2_2SearcherCanFillInIDs(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "../testdata/project2/"
+ config := &Config2_2{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ }
+
+ doc, err := BuildIDsDocument2_2(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking size of each
+ if doc.Packages == nil {
+ t.Fatalf("expected non-nil Packages, got nil")
+ }
+ if len(doc.Packages) != 1 {
+ t.Fatalf("expected Packages len to be 1, got %d", len(doc.Packages))
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil Files, got nil")
+ }
+ if len(pkg.Files) != 6 {
+ t.Fatalf("expected Files len to be 6, got %d", len(pkg.Files))
+ }
+
+ fileInFolder := pkg.Files[0]
+ if fileInFolder.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileInFolder.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileInFolder.LicenseInfoInFiles))
+ }
+ if fileInFolder.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseInfoInFiles[0])
+ }
+ if fileInFolder.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseConcluded)
+ }
+
+ fileTrailingComment := pkg.Files[1]
+ if fileTrailingComment.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileTrailingComment.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileTrailingComment.LicenseInfoInFiles))
+ }
+ if fileTrailingComment.LicenseInfoInFiles[0] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseInfoInFiles[0])
+ }
+ if fileTrailingComment.LicenseConcluded != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseConcluded)
+ }
+
+ fileHasDuplicateID := pkg.Files[2]
+ if fileHasDuplicateID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasDuplicateID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileHasDuplicateID.LicenseInfoInFiles))
+ }
+ if fileHasDuplicateID.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseInfoInFiles[0])
+ }
+ if fileHasDuplicateID.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseConcluded)
+ }
+
+ fileHasID := pkg.Files[3]
+ if fileHasID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasID.LicenseInfoInFiles) != 2 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 2, got %d", len(fileHasID.LicenseInfoInFiles))
+ }
+ if fileHasID.LicenseInfoInFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", fileHasID.LicenseInfoInFiles[0])
+ }
+ if fileHasID.LicenseInfoInFiles[1] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileHasID.LicenseInfoInFiles[1])
+ }
+ if fileHasID.LicenseConcluded != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "Apache-2.0 OR GPL-2.0-or-later", fileHasID.LicenseConcluded)
+ }
+
+ fileMultipleIDs := pkg.Files[4]
+ if fileMultipleIDs.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileMultipleIDs.LicenseInfoInFiles) != 5 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 5, got %d", len(fileMultipleIDs.LicenseInfoInFiles))
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[0] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", fileMultipleIDs.LicenseInfoInFiles[0])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[1] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", fileMultipleIDs.LicenseInfoInFiles[1])
+ }
+ // here, DO NOT keep the +
+ if fileMultipleIDs.LicenseInfoInFiles[2] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", fileMultipleIDs.LicenseInfoInFiles[2])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[3] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", fileMultipleIDs.LicenseInfoInFiles[3])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[4] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileMultipleIDs.LicenseInfoInFiles[4])
+ }
+ if fileMultipleIDs.LicenseConcluded != "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+" {
+ t.Errorf("expected %v, got %v", "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+", fileMultipleIDs.LicenseConcluded)
+ }
+
+ fileNoID := pkg.Files[5]
+ if fileNoID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileNoID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileNoID.LicenseInfoInFiles))
+ }
+ if fileNoID.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseInfoInFiles[0])
+ }
+ if fileNoID.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseConcluded)
+ }
+
+ // and finally, the package should have all of these licenses
+ if pkg.PackageLicenseInfoFromFiles == nil {
+ t.Fatalf("expected non-nil PackageLicenseInfoFromFiles, got nil")
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 7 {
+ t.Fatalf("expected PackageLicenseInfoFromFiles len to be 7, got %d", len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseInfoFromFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", pkg.PackageLicenseInfoFromFiles[0])
+ }
+ if pkg.PackageLicenseInfoFromFiles[1] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", pkg.PackageLicenseInfoFromFiles[1])
+ }
+ if pkg.PackageLicenseInfoFromFiles[2] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", pkg.PackageLicenseInfoFromFiles[2])
+ }
+ // here, DO NOT keep the +
+ if pkg.PackageLicenseInfoFromFiles[3] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", pkg.PackageLicenseInfoFromFiles[3])
+ }
+ if pkg.PackageLicenseInfoFromFiles[4] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", pkg.PackageLicenseInfoFromFiles[4])
+ }
+ if pkg.PackageLicenseInfoFromFiles[5] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", pkg.PackageLicenseInfoFromFiles[5])
+ }
+ if pkg.PackageLicenseInfoFromFiles[6] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", pkg.PackageLicenseInfoFromFiles[6])
+ }
+
+}
+
+func Test2_2SearcherCanFillInIDsAndIgnorePaths(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../testdata/project3/"
+ config := &Config2_2{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ BuilderPathsIgnored: []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ },
+ SearcherPathsIgnored: []string{
+ "**/dontscan.txt",
+ },
+ }
+
+ doc, err := BuildIDsDocument2_2(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking licenses for each, and
+ // confirming NOASSERTION for those that are skipped
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected len %d, got %d", 5, len(pkg.Files))
+ }
+
+ f := pkg.Files[0]
+ if f.FileName != "./dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[1]
+ if f.FileName != "./keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[2]
+ if f.FileName != "./keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[3]
+ if f.FileName != "./subdir/keep/dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[4]
+ if f.FileName != "./subdir/keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+}
+
+func Test2_2SearcherFailsWithInvalidPath(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "./oops/invalid"
+ config := &Config2_2{
+ NamespacePrefix: "whatever",
+ }
+
+ _, err := BuildIDsDocument2_2(packageName, dirRoot, config)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.3 Searcher top-level function tests =====
+func Test2_3SearcherCanFillInIDs(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "../testdata/project2/"
+ config := &Config2_3{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ }
+
+ doc, err := BuildIDsDocument2_3(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking size of each
+ if doc.Packages == nil {
+ t.Fatalf("expected non-nil Packages, got nil")
+ }
+ if len(doc.Packages) != 1 {
+ t.Fatalf("expected Packages len to be 1, got %d", len(doc.Packages))
+ }
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+
+ if pkg.Files == nil {
+ t.Fatalf("expected non-nil Files, got nil")
+ }
+ if len(pkg.Files) != 6 {
+ t.Fatalf("expected Files len to be 6, got %d", len(pkg.Files))
+ }
+
+ fileInFolder := pkg.Files[0]
+ if fileInFolder.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileInFolder.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileInFolder.LicenseInfoInFiles))
+ }
+ if fileInFolder.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseInfoInFiles[0])
+ }
+ if fileInFolder.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileInFolder.LicenseConcluded)
+ }
+
+ fileTrailingComment := pkg.Files[1]
+ if fileTrailingComment.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileTrailingComment.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileTrailingComment.LicenseInfoInFiles))
+ }
+ if fileTrailingComment.LicenseInfoInFiles[0] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseInfoInFiles[0])
+ }
+ if fileTrailingComment.LicenseConcluded != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileTrailingComment.LicenseConcluded)
+ }
+
+ fileHasDuplicateID := pkg.Files[2]
+ if fileHasDuplicateID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasDuplicateID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileHasDuplicateID.LicenseInfoInFiles))
+ }
+ if fileHasDuplicateID.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseInfoInFiles[0])
+ }
+ if fileHasDuplicateID.LicenseConcluded != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileHasDuplicateID.LicenseConcluded)
+ }
+
+ fileHasID := pkg.Files[3]
+ if fileHasID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileHasID.LicenseInfoInFiles) != 2 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 2, got %d", len(fileHasID.LicenseInfoInFiles))
+ }
+ if fileHasID.LicenseInfoInFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", fileHasID.LicenseInfoInFiles[0])
+ }
+ if fileHasID.LicenseInfoInFiles[1] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", fileHasID.LicenseInfoInFiles[1])
+ }
+ if fileHasID.LicenseConcluded != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "Apache-2.0 OR GPL-2.0-or-later", fileHasID.LicenseConcluded)
+ }
+
+ fileMultipleIDs := pkg.Files[4]
+ if fileMultipleIDs.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileMultipleIDs.LicenseInfoInFiles) != 5 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 5, got %d", len(fileMultipleIDs.LicenseInfoInFiles))
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[0] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", fileMultipleIDs.LicenseInfoInFiles[0])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[1] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", fileMultipleIDs.LicenseInfoInFiles[1])
+ }
+ // here, DO NOT keep the +
+ if fileMultipleIDs.LicenseInfoInFiles[2] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", fileMultipleIDs.LicenseInfoInFiles[2])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[3] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", fileMultipleIDs.LicenseInfoInFiles[3])
+ }
+ if fileMultipleIDs.LicenseInfoInFiles[4] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", fileMultipleIDs.LicenseInfoInFiles[4])
+ }
+ if fileMultipleIDs.LicenseConcluded != "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+" {
+ t.Errorf("expected %v, got %v", "((MIT AND BSD-3-Clause) OR ISC) AND BSD-2-Clause AND EPL-1.0+", fileMultipleIDs.LicenseConcluded)
+ }
+
+ fileNoID := pkg.Files[5]
+ if fileNoID.LicenseInfoInFiles == nil {
+ t.Fatalf("expected non-nil LicenseInfoInFiles, got nil")
+ }
+ if len(fileNoID.LicenseInfoInFiles) != 1 {
+ t.Fatalf("expected LicenseInfoInFiles len to be 1, got %d", len(fileNoID.LicenseInfoInFiles))
+ }
+ if fileNoID.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseInfoInFiles[0])
+ }
+ if fileNoID.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %v, got %v", "NOASSERTION", fileNoID.LicenseConcluded)
+ }
+
+ // and finally, the package should have all of these licenses
+ if pkg.PackageLicenseInfoFromFiles == nil {
+ t.Fatalf("expected non-nil PackageLicenseInfoFromFiles, got nil")
+ }
+ if len(pkg.PackageLicenseInfoFromFiles) != 7 {
+ t.Fatalf("expected PackageLicenseInfoFromFiles len to be 7, got %d", len(pkg.PackageLicenseInfoFromFiles))
+ }
+ if pkg.PackageLicenseInfoFromFiles[0] != "Apache-2.0" {
+ t.Errorf("expected %v, got %v", "Apache-2.0", pkg.PackageLicenseInfoFromFiles[0])
+ }
+ if pkg.PackageLicenseInfoFromFiles[1] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", pkg.PackageLicenseInfoFromFiles[1])
+ }
+ if pkg.PackageLicenseInfoFromFiles[2] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", pkg.PackageLicenseInfoFromFiles[2])
+ }
+ // here, DO NOT keep the +
+ if pkg.PackageLicenseInfoFromFiles[3] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", pkg.PackageLicenseInfoFromFiles[3])
+ }
+ if pkg.PackageLicenseInfoFromFiles[4] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", pkg.PackageLicenseInfoFromFiles[4])
+ }
+ if pkg.PackageLicenseInfoFromFiles[5] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", pkg.PackageLicenseInfoFromFiles[5])
+ }
+ if pkg.PackageLicenseInfoFromFiles[6] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", pkg.PackageLicenseInfoFromFiles[6])
+ }
+
+}
+
+func Test2_3SearcherCanFillInIDsAndIgnorePaths(t *testing.T) {
+ packageName := "project3"
+ dirRoot := "../testdata/project3/"
+ config := &Config2_3{
+ NamespacePrefix: "https://github.com/swinslow/spdx-docs/spdx-go/testdata-",
+ BuilderPathsIgnored: []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ },
+ SearcherPathsIgnored: []string{
+ "**/dontscan.txt",
+ },
+ }
+
+ doc, err := BuildIDsDocument2_3(packageName, dirRoot, config)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if doc == nil {
+ t.Fatalf("expected non-nil Document, got nil")
+ }
+
+ // not checking all contents of doc, see builder tests for those
+
+ // get the package and its files, checking licenses for each, and
+ // confirming NOASSERTION for those that are skipped
+ pkg := doc.Packages[0]
+ if pkg == nil {
+ t.Fatalf("expected non-nil pkg, got nil")
+ }
+ if len(pkg.Files) != 5 {
+ t.Fatalf("expected len %d, got %d", 5, len(pkg.Files))
+ }
+
+ f := pkg.Files[0]
+ if f.FileName != "./dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[1]
+ if f.FileName != "./keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[2]
+ if f.FileName != "./keep.txt" {
+ t.Errorf("expected %v, got %v", "./keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[3]
+ if f.FileName != "./subdir/keep/dontscan.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/dontscan.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", f.LicenseConcluded)
+ }
+
+ f = pkg.Files[4]
+ if f.FileName != "./subdir/keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "./subdir/keep/keep.txt", f.FileName)
+ }
+ if len(f.LicenseInfoInFiles) != 1 {
+ t.Errorf("expected len to be %d, got %d", 1, len(f.LicenseInfoInFiles))
+ }
+ if f.LicenseInfoInFiles[0] != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseInfoInFiles[0])
+ }
+ if f.LicenseConcluded != "MIT" {
+ t.Errorf("expected %s, got %s", "MIT", f.LicenseConcluded)
+ }
+}
+
+func Test2_3SearcherFailsWithInvalidPath(t *testing.T) {
+ packageName := "project2"
+ dirRoot := "./oops/invalid"
+ config := &Config2_3{
+ NamespacePrefix: "whatever",
+ }
+
+ _, err := BuildIDsDocument2_3(packageName, dirRoot, config)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== Searcher utility tests =====
+func TestCanFindShortFormIDWhenPresent(t *testing.T) {
+ filePath := "../testdata/project2/has-id.txt"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 1 {
+ t.Fatalf("expected len 1, got %d", len(ids))
+ }
+
+ if ids[0] != "Apache-2.0 OR GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "Apache-2.0 OR GPL-2.0-or-later", ids[0])
+ }
+}
+
+func TestCanFindMultipleShortFormIDsWhenPresent(t *testing.T) {
+ filePath := "../testdata/project2/has-multiple-ids.txt"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 3 {
+ t.Fatalf("expected len 3, got %d", len(ids))
+ }
+
+ if ids[0] != "(MIT AND BSD-3-Clause) OR ISC" {
+ t.Errorf("expected %v, got %v", "(MIT AND BSD-3-Clause) OR ISC", ids[0])
+ }
+ if ids[1] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", ids[1])
+ }
+ if ids[2] != "EPL-1.0+" {
+ t.Errorf("expected %v, got %v", "EPL-1.0+", ids[2])
+ }
+}
+
+func TestCanCollapseDuplicateShortFormIDsWhenPresent(t *testing.T) {
+ filePath := "../testdata/project2/has-duplicate-ids.txt"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 1 {
+ t.Fatalf("expected len 1, got %d", len(ids))
+ }
+
+ if ids[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", ids[0])
+ }
+}
+
+func TestCanStripTrailingStarSlash(t *testing.T) {
+ filePath := "../testdata/project2/folder/has-trailing-comment-marker.c"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 1 {
+ t.Fatalf("expected len 1, got %d", len(ids))
+ }
+
+ if ids[0] != "GPL-2.0-or-later" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-or-later", ids[0])
+ }
+}
+
+func TestCanIgnoreShortFormIDWhenTooManyPrefixChars(t *testing.T) {
+ filePath := "../testdata/project4/has-id-to-ignore.txt"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 0 {
+ t.Fatalf("expected len 0, got %d", len(ids))
+ }
+}
+
+func TestCanPickJustTheRightID(t *testing.T) {
+ filePath := "../testdata/project4/has-mix-of-ids.txt"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 1 {
+ t.Fatalf("expected len 1, got %d", len(ids))
+ }
+
+ if ids[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", ids[0])
+ }
+}
+
+func TestCannotFindShortFormIDWhenAbsent(t *testing.T) {
+ filePath := "../testdata/project2/no-id.txt"
+
+ ids, err := searchFileIDs(filePath)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+
+ if len(ids) != 0 {
+ t.Fatalf("expected len 0, got %d", len(ids))
+ }
+}
+
+func TestCanExcludeTrashCharactersFromID(t *testing.T) {
+ lid := "Apac\",he-2.0"
+ want := "Apache-2.0"
+ got := stripTrash(lid)
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+
+ lid = "Apache-2.0"
+ want = "Apache-2.0"
+ got = stripTrash(lid)
+ if want != got {
+ t.Errorf("expected %v, got %v", want, got)
+ }
+}
+
+func TestSearchFileIDsFailsWithInvalidFilePath(t *testing.T) {
+ filePath := "./oops/nm/invalid"
+
+ _, err := searchFileIDs(filePath)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func TestWillParenthesizeIfNeeded(t *testing.T) {
+ licID := "MIT OR BSD-3-Clause"
+ retval := makeElement(licID)
+ if retval != "(MIT OR BSD-3-Clause)" {
+ t.Errorf("expected %v, got %v", "(MIT OR BSD-3-Clause)", retval)
+ }
+
+ licID = "ISC AND HPND"
+ retval = makeElement(licID)
+ if retval != "(ISC AND HPND)" {
+ t.Errorf("expected %v, got %v", "(ISC AND HPND)", retval)
+ }
+}
+
+func TestWillNotParenthesizeIfNotNeeded(t *testing.T) {
+ lic := "MIT"
+ retval := makeElement(lic)
+ if retval != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", retval)
+ }
+
+ lic = "GPL-2.0-only WITH Classpath-exception-2.0"
+ retval = makeElement(lic)
+ if retval != "GPL-2.0-only WITH Classpath-exception-2.0" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-only WITH Classpath-exception-2.0", retval)
+ }
+}
+
+func TestCanGetIndividualLicenses(t *testing.T) {
+ // single license
+ lic := "MIT"
+ lics := getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ if len(lics) != 1 {
+ t.Fatalf("expected lics to have len 1, got %d", len(lics))
+ }
+ if lics[0] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", lics[0])
+ }
+
+ // two-license combo
+ lic = "ISC AND BSD-3-Clause"
+ lics = getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ if len(lics) != 2 {
+ t.Fatalf("expected lics to have len 2, got %d", len(lics))
+ }
+ // should be sorted alphabetically
+ if lics[0] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", lics[0])
+ }
+ if lics[1] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", lics[1])
+ }
+
+ // license WITH exception
+ lic = "GPL-2.0-only WITH Classpath-exception-2.0"
+ lics = getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ if len(lics) != 2 {
+ t.Fatalf("expected lics to have len 2, got %d", len(lics))
+ }
+ // exception should be listed separately
+ if lics[0] != "Classpath-exception-2.0" {
+ t.Errorf("expected %v, got %v", "Classpath-exception-2.0", lics[0])
+ }
+ if lics[1] != "GPL-2.0-only" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-only", lics[1])
+ }
+
+ // two-license combo with parens
+ lic = "(JSON OR BSD-2-Clause)"
+ lics = getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ if len(lics) != 2 {
+ t.Fatalf("expected lics to have len 2, got %d", len(lics))
+ }
+ // parens should get dropped
+ if lics[0] != "BSD-2-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-2-Clause", lics[0])
+ }
+ if lics[1] != "JSON" {
+ t.Errorf("expected %v, got %v", "JSON", lics[1])
+ }
+
+ // multi-license combo with nested parens
+ lic = "GPL-2.0-only AND ((EPL-1.0 AND BSD-4-Clause) OR MIT)"
+ lics = getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ if len(lics) != 4 {
+ t.Fatalf("expected lics to have len 4, got %d", len(lics))
+ }
+ if lics[0] != "BSD-4-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-4-Clause", lics[0])
+ }
+ if lics[1] != "EPL-1.0" {
+ t.Errorf("expected %v, got %v", "EPL-1.0", lics[1])
+ }
+ if lics[2] != "GPL-2.0-only" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-only", lics[2])
+ }
+ if lics[3] != "MIT" {
+ t.Errorf("expected %v, got %v", "MIT", lics[3])
+ }
+}
+
+func TestCanGetIndividualLicensesIgnoringOperatorCase(t *testing.T) {
+ // two-license combo with lowercase 'and'
+ lic := "ISC and BSD-3-Clause"
+ lics := getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ // should be sorted alphabetically; 'and' should not appear
+ if len(lics) != 2 {
+ t.Fatalf("expected lics to have len 2, got %d", len(lics))
+ }
+ if lics[0] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", lics[0])
+ }
+ if lics[1] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", lics[1])
+ }
+
+ // two-license combo with lowercase 'or'
+ lic = "ISC or BSD-3-Clause"
+ lics = getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ // should be sorted alphabetically; 'or' should not appear
+ if len(lics) != 2 {
+ t.Fatalf("expected lics to have len 2, got %d", len(lics))
+ }
+ if lics[0] != "BSD-3-Clause" {
+ t.Errorf("expected %v, got %v", "BSD-3-Clause", lics[0])
+ }
+ if lics[1] != "ISC" {
+ t.Errorf("expected %v, got %v", "ISC", lics[1])
+ }
+
+ // two-license combo with lowercase 'with'
+ lic = "GPL-2.0-only with Classpath-exception-2.0"
+ lics = getIndividualLicenses(lic)
+ if lics == nil {
+ t.Fatalf("expected non-nil lics, got nil")
+ }
+ // should be sorted alphabetically; 'with' should not appear
+ if len(lics) != 2 {
+ t.Fatalf("expected lics to have len 2, got %d", len(lics))
+ }
+ if lics[0] != "Classpath-exception-2.0" {
+ t.Errorf("expected %v, got %v", "Classpath-exception-2.0", lics[0])
+ }
+ if lics[1] != "GPL-2.0-only" {
+ t.Errorf("expected %v, got %v", "GPL-2.0-only", lics[1])
+ }
+
+}
diff --git a/json/json_v2_2_test.go b/json/json_v2_2_test.go
new file mode 100644
index 0000000..01400ec
--- /dev/null
+++ b/json/json_v2_2_test.go
@@ -0,0 +1,451 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_json
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func TestLoad2_2(t *testing.T) {
+ file, err := os.Open("../examples/sample-docs/json/SPDXJSONExample-v2.2.spdx.json")
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := Load2_2(file)
+ if err != nil {
+ t.Errorf("json.parser.Load2_2() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_2
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("Got incorrect struct after parsing JSON example")
+ return
+ }
+}
+
+func TestWrite2_2(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_2
+ if err := Save2_2(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_2() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original struct we wrote
+ parsedDoc, err := Load2_2(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("Got incorrect struct after writing and re-parsing JSON example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example JSON SPDX v2.2 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want2_2 = v2_2.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.2",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_2.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_2.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_2.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_2.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_2.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_2.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_2.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_2.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",
+ },
+ },
+ Files: []*v2_2.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_2.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_2.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_2.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",
+ },
+ },
+}
diff --git a/json/json_v2_3_test.go b/json/json_v2_3_test.go
new file mode 100644
index 0000000..2cefafc
--- /dev/null
+++ b/json/json_v2_3_test.go
@@ -0,0 +1,490 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_json
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+var update = *flag.Bool("update-snapshots", false, "update the example snapshot")
+
+func TestLoad2_3(t *testing.T) {
+ fileName := "../examples/sample-docs/json/SPDXJSONExample-v2.3.spdx.json"
+
+ if update {
+ data, err := json.MarshalIndent(want2_3, "", " ")
+ if err != nil {
+ t.Errorf("unable to serialize SPDX 2.3 example to JSON: %v", err)
+ }
+ err = os.WriteFile(fileName, data, 0644)
+ if err != nil {
+ t.Errorf("unable to write SPDX 2.3 example to JSON: %v", err)
+ }
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := Load2_3(file)
+ if err != nil {
+ t.Errorf("json.parser.Load2_3() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("Got incorrect struct after parsing JSON example")
+ return
+ }
+}
+
+func TestWrite2_3(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+ if err := Save2_3(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_3() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original struct we wrote
+ parsedDoc, err := Load2_3(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("Got incorrect struct after writing and re-parsing JSON example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example JSON SPDX v2.3 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want2_3 = v2_3.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.3",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {CreatorType: "Tool", Creator: "LicenseFind-1.0"},
+ {CreatorType: "Organization", Creator: "ExampleCodeInspect ()"},
+ {CreatorType: "Person", Creator: "Jane Doe ()"},
+ },
+ Created: "2010-01-29T18:30:22Z",
+ CreatorComment: "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ },
+ DocumentComment: "This document was created using SPDX 2.0 using licenses from the web site.",
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{
+ {
+ DocumentRefID: "DocumentRef-spdx-tool-1.2",
+ URI: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ Checksum: common.Checksum{
+ Algorithm: common.SHA1,
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2759",
+ },
+ },
+ },
+ OtherLicenses: []*v2_3.OtherLicense{
+ {
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-4",
+ ExtractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-Beerware-4.2",
+ ExtractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ LicenseComment: "The beerware license has a couple of other standard variants.",
+ LicenseName: "Beer-Ware License (Version 42)",
+ LicenseCrossReferences: []string{"http://people.freebsd.org/~phk/"},
+ },
+ {
+ LicenseIdentifier: "LicenseRef-3",
+ ExtractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ LicenseName: "CyberNeko License",
+ LicenseCrossReferences: []string{
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com",
+ },
+ LicenseComment: "This is tye CyperNeko License",
+ },
+ },
+ Annotations: []*v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Jane Doe ()",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Document level annotation",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Joe Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-02-10T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Suzanne Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-03-13T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "Another example reviewer.",
+ },
+ },
+ Packages: []*v2_3.Package{
+ {
+ PackageName: "glibc",
+ PackageSPDXIdentifier: "SPDXRef-Package",
+ PackageVersion: "2.11.1",
+ PackageFileName: "glibc-2.11.1.tar.gz",
+ PackageSupplier: &common.Supplier{
+ Supplier: "Jane Doe (jane.doe@example.com)",
+ SupplierType: "Person",
+ },
+ PackageOriginator: &common.Originator{
+ Originator: "ExampleCodeInspect (contact@example.com)",
+ OriginatorType: "Organization",
+ },
+ PackageDownloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ FilesAnalyzed: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ ExcludedFiles: []string{"./package.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: "SHA256",
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ },
+ PackageHomePage: "http://ftp.gnu.org/gnu/glibc",
+ PackageSourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ PackageLicenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)",
+ PackageLicenseInfoFromFiles: []string{
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1",
+ },
+ PackageLicenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)",
+ PackageLicenseComments: "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ PackageCopyrightText: "Copyright 2008-2010 John Smith",
+ PackageSummary: "GNU C library.",
+ PackageDescription: "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ PackageComment: "",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "SECURITY",
+ RefType: "cpe23Type",
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ },
+ {
+ Category: "OTHER",
+ RefType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ Locator: "acmecorp/acmenator/4.1.3-alpha",
+ ExternalRefComment: "This is the external ref for Acme",
+ },
+ },
+ PackageAttributionTexts: []string{
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.",
+ },
+ Files: nil,
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "PACKAGE-MANAGER",
+ RefType: "purl",
+ Locator: "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ },
+ },
+ FilesAnalyzed: false,
+ PackageHomePage: "http://www.openjena.org/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageVersion: "3.12.0",
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-Saxon",
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ PackageCopyrightText: "Copyright Saxonica Ltd",
+ PackageDescription: "The Saxon package is a collection of tools for processing XML documents.",
+ PackageDownloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://saxon.sourceforge.net/",
+ PackageLicenseComments: "Other versions available for a commercial license",
+ PackageLicenseConcluded: "MPL-1.0",
+ PackageLicenseDeclared: "MPL-1.0",
+ PackageName: "Saxon",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "8.8",
+ },
+ {
+ PrimaryPackagePurpose: "CONTAINER",
+ PackageSPDXIdentifier: "SPDXRef-CentOS-7",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDescription: "The CentOS container used to run the application.",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "https://www.centos.org/",
+ PackageName: "centos",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "centos7.9.2009",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ },
+ },
+ Files: []*v2_3.File{
+ {
+ FileName: "./src/org/spdx/parser/DOAPProject.java",
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ FileTypes: []string{
+ "SOURCE",
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright 2010, 2011 Source Auditor Inc.",
+ FileContributors: []string{
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c",
+ },
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-CommonsLangSrc",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file is used by Jena",
+ FileCopyrightText: "Copyright 2001-2011 The Apache Software Foundation",
+ FileContributors: []string{"Apache Software Foundation"},
+ FileName: "./lib-source/commons-lang3-3.1-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileNotice: "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-JenaLib",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file belongs to Jena",
+ FileCopyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ FileContributors: []string{"Apache Software Foundation", "Hewlett Packard Inc."},
+ FileName: "./lib-source/jena-2.6.3-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseComments: "This license is used by Jena",
+ LicenseConcluded: "LicenseRef-1",
+ LicenseInfoInFiles: []string{"LicenseRef-1"},
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-File",
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "File Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "File level annotation",
+ },
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ },
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ FileComment: "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ FileCopyrightText: "Copyright 2008-2010 John Smith",
+ FileContributors: []string{"The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"},
+ FileName: "./package/foo.c",
+ FileTypes: []string{"SOURCE"},
+ LicenseComments: "The concluded license was taken from the package level that the file was included in.",
+ LicenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)",
+ LicenseInfoInFiles: []string{"GPL-2.0-only", "LicenseRef-2"},
+ FileNotice: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ },
+ },
+ Snippets: []v2_3.Snippet{
+ {
+ SnippetSPDXIdentifier: "SPDXRef-Snippet",
+ SnippetFromFileSPDXIdentifier: "SPDXRef-DoapSource",
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{
+ Offset: 310,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ Offset: 420,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ {
+ StartPointer: common.SnippetRangePointer{
+ LineNumber: 5,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ LineNumber: 23,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-only",
+ LicenseInfoInSnippet: []string{"GPL-2.0-only"},
+ SnippetLicenseComments: "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ SnippetCopyrightText: "Copyright 2008-2010 John Smith",
+ SnippetComment: "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ SnippetName: "from linux kernel",
+ },
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("spdx-tool-1.2", "ToolsElement"),
+ Relationship: "COPY_OF",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "JenaLib"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "Saxon"),
+ Relationship: "DYNAMIC_LINK",
+ },
+ {
+ RefA: common.MakeDocElementID("", "CommonsLangSrc"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "GENERATED_FROM",
+ },
+ {
+ RefA: common.MakeDocElementID("", "JenaLib"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "File"),
+ RefB: common.MakeDocElementID("", "fromDoap-0"),
+ Relationship: "GENERATED_FROM",
+ },
+ },
+ Reviews: []*v2_3.Review{
+ {
+ Reviewer: "joe@example.com",
+ ReviewerType: "Person",
+ ReviewDate: "2021-11-03T05:43:21Z",
+ ReviewComment: "This is a review comment",
+ },
+ },
+}
diff --git a/json/parser.go b/json/parser.go
new file mode 100644
index 0000000..ee7915d
--- /dev/null
+++ b/json/parser.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_json
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// Load2_2 takes in an io.Reader and returns an SPDX document.
+func Load2_2(content io.Reader) (*v2_2.Document, error) {
+ // convert io.Reader to a slice of bytes and call the parser
+ buf := new(bytes.Buffer)
+ _, err := buf.ReadFrom(content)
+ if err != nil {
+ return nil, err
+ }
+
+ var doc v2_2.Document
+ err = json.Unmarshal(buf.Bytes(), &doc)
+ if err != nil {
+ return nil, err
+ }
+
+ return &doc, nil
+}
+
+// Load2_3 takes in an io.Reader and returns an SPDX document.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ // convert io.Reader to a slice of bytes and call the parser
+ buf := new(bytes.Buffer)
+ _, err := buf.ReadFrom(content)
+ if err != nil {
+ return nil, err
+ }
+
+ var doc v2_3.Document
+ err = json.Unmarshal(buf.Bytes(), &doc)
+ if err != nil {
+ return nil, err
+ }
+
+ return &doc, nil
+}
diff --git a/json/writer.go b/json/writer.go
new file mode 100644
index 0000000..8f2b94d
--- /dev/null
+++ b/json/writer.go
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_json
+
+import (
+ "encoding/json"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// Save2_2 takes an SPDX Document (version 2.2) and an io.Writer, and writes the document to the writer in JSON format.
+func Save2_2(doc *v2_2.Document, w io.Writer) error {
+ buf, err := json.Marshal(doc)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Save2_3 takes an SPDX Document (version 2.2) and an io.Writer, and writes the document to the writer in JSON format.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ buf, err := json.Marshal(doc)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/licensediff/licensediff.go b/licensediff/licensediff.go
new file mode 100644
index 0000000..ba8cd84
--- /dev/null
+++ b/licensediff/licensediff.go
@@ -0,0 +1,139 @@
+// Package licensediff is used to generate a "diff" between the concluded
+// licenses in two SPDX Packages, using the filename as the match point.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package licensediff
+
+import (
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// LicensePair is a result set where we are talking about two license strings,
+// potentially differing, for a single filename between two SPDX Packages.
+type LicensePair struct {
+ First string
+ Second string
+}
+
+// MakePairs2_1 essentially just consolidates all files and LicenseConcluded
+// strings into a single data structure.
+func MakePairs2_1(p1 *v2_1.Package, p2 *v2_1.Package) (map[string]LicensePair, error) {
+ pairs := map[string]LicensePair{}
+
+ // first, go through and add all files/licenses from p1
+ for _, f := range p1.Files {
+ pair := LicensePair{First: f.LicenseConcluded, Second: ""}
+ pairs[f.FileName] = pair
+ }
+
+ // now, go through all files/licenses from p2. If already
+ // present, add as .second; if not, create new pair
+ for _, f := range p2.Files {
+ firstLic := ""
+ existingPair, ok := pairs[f.FileName]
+ if ok {
+ // already present; update it
+ firstLic = existingPair.First
+ }
+ // now, update what's there, either way
+ pair := LicensePair{First: firstLic, Second: f.LicenseConcluded}
+ pairs[f.FileName] = pair
+ }
+
+ return pairs, nil
+}
+
+// MakePairs2_2 essentially just consolidates all files and LicenseConcluded
+// strings into a single data structure.
+func MakePairs2_2(p1 *v2_2.Package, p2 *v2_2.Package) (map[string]LicensePair, error) {
+ pairs := map[string]LicensePair{}
+
+ // first, go through and add all files/licenses from p1
+ for _, f := range p1.Files {
+ pair := LicensePair{First: f.LicenseConcluded, Second: ""}
+ pairs[f.FileName] = pair
+ }
+
+ // now, go through all files/licenses from p2. If already
+ // present, add as .second; if not, create new pair
+ for _, f := range p2.Files {
+ firstLic := ""
+ existingPair, ok := pairs[f.FileName]
+ if ok {
+ // already present; update it
+ firstLic = existingPair.First
+ }
+ // now, update what's there, either way
+ pair := LicensePair{First: firstLic, Second: f.LicenseConcluded}
+ pairs[f.FileName] = pair
+ }
+
+ return pairs, nil
+}
+
+// MakePairs2_3 essentially just consolidates all files and LicenseConcluded
+// strings into a single data structure.
+func MakePairs2_3(p1 *v2_3.Package, p2 *v2_3.Package) (map[string]LicensePair, error) {
+ pairs := map[string]LicensePair{}
+
+ // first, go through and add all files/licenses from p1
+ for _, f := range p1.Files {
+ pair := LicensePair{First: f.LicenseConcluded, Second: ""}
+ pairs[f.FileName] = pair
+ }
+
+ // now, go through all files/licenses from p2. If already
+ // present, add as .second; if not, create new pair
+ for _, f := range p2.Files {
+ firstLic := ""
+ existingPair, ok := pairs[f.FileName]
+ if ok {
+ // already present; update it
+ firstLic = existingPair.First
+ }
+ // now, update what's there, either way
+ pair := LicensePair{First: firstLic, Second: f.LicenseConcluded}
+ pairs[f.FileName] = pair
+ }
+
+ return pairs, nil
+}
+
+// LicenseDiff is a structured version of the output of MakePairs. It is
+// meant to make it easier to find and report on, e.g., just the files that
+// have different licenses, or those that are in just one scan.
+type LicenseDiff struct {
+ InBothChanged map[string]LicensePair
+ InBothSame map[string]string
+ InFirstOnly map[string]string
+ InSecondOnly map[string]string
+}
+
+// MakeResults creates a more structured set of results from the output
+// of MakePairs.
+func MakeResults(pairs map[string]LicensePair) (*LicenseDiff, error) {
+ diff := &LicenseDiff{
+ InBothChanged: map[string]LicensePair{},
+ InBothSame: map[string]string{},
+ InFirstOnly: map[string]string{},
+ InSecondOnly: map[string]string{},
+ }
+
+ // walk through pairs and allocate them where they belong
+ for filename, pair := range pairs {
+ if pair.First == pair.Second {
+ diff.InBothSame[filename] = pair.First
+ } else {
+ if pair.First == "" {
+ diff.InSecondOnly[filename] = pair.Second
+ } else if pair.Second == "" {
+ diff.InFirstOnly[filename] = pair.First
+ } else {
+ diff.InBothChanged[filename] = pair
+ }
+ }
+ }
+
+ return diff, nil
+}
diff --git a/licensediff/licensediff_test.go b/licensediff/licensediff_test.go
new file mode 100644
index 0000000..226ad51
--- /dev/null
+++ b/licensediff/licensediff_test.go
@@ -0,0 +1,1606 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package licensediff
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 License diff top-level function tests =====
+func Test2_1DifferCanCreateDiffPairs(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_1.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3", Algorithm: common.SHA1}},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_1.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{Value: "066c5139bd9a43d15812ec1a1755b08ccf199824", Algorithm: common.SHA1}},
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_1.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{Value: "bd0f4863b15fad2b79b35303af54fcb5baaf7c68", Algorithm: common.SHA1}},
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_1.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{Value: "bc417a575ceae93435bcb7bfd382ac28cbdaa8b5", Algorithm: common.SHA1}},
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_1.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{Value: "bc417a575ceae93435bcb7bfd382ac28cbdaa8b5", Algorithm: common.SHA1}},
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_1.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{Value: "ba226db943bbbf455da77afab6f16dbab156d000", Algorithm: common.SHA1}},
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_1.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{Value: "b6e0ec7d085c5699b46f6f8d425413702652874d", Algorithm: common.SHA1}},
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_1.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{Value: "ba226db943bbbf455da77afab6f16dbab156d000", Algorithm: common.SHA1}},
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_1.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{Value: "b6e0ec7d085c5699b46f6f8d425413702652874d", Algorithm: common.SHA1}},
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_1.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_1.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_1.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_1.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_1(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are what we expect
+ // there should be 6 entries, one for each unique filename
+ if len(diffMap) != 6 {
+ t.Fatalf("Expected %d, got %d", 6, len(diffMap))
+ }
+
+ // check each filename is present, and check its pair
+ // pair 1 -- same in both
+ pair1, ok := diffMap["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair1")
+ }
+ if pair1.First != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, pair1.First)
+ }
+ if pair1.Second != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair1.Second)
+ }
+
+ // pair 2 -- only in first
+ pair2, ok := diffMap["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair2")
+ }
+ if pair2.First != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair2.First)
+ }
+ if pair2.Second != "" {
+ t.Errorf("Expected %s, got %s", "", pair2.Second)
+ }
+
+ // pair 3 -- only in second
+ pair3, ok := diffMap["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair3")
+ }
+ if pair3.First != "" {
+ t.Errorf("Expected %s, got %s", "", pair3.First)
+ }
+ if pair3.Second != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, pair3.Second)
+ }
+
+ // pair 4 -- in both but different license
+ pair4, ok := diffMap["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair4")
+ }
+ if pair4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, pair4.First)
+ }
+ if pair4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, pair4.Second)
+ }
+
+ // pair 5 -- in both but different hash, same license
+ pair5, ok := diffMap["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair5")
+ }
+ if pair5.First != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, pair5.First)
+ }
+ if pair5.Second != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, pair5.Second)
+ }
+
+ // pair 6 -- in both but different hash, different license
+ pair6, ok := diffMap["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair6")
+ }
+ if pair6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, pair6.First)
+ }
+ if pair6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, pair6.Second)
+ }
+}
+
+func Test2_1DifferCanCreateDiffStructuredResults(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_1.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3", Algorithm: common.SHA1}},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_1.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{Value: "066c5139bd9a43d15812ec1a1755b08ccf199824", Algorithm: common.SHA1}},
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_1.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{Value: "bd0f4863b15fad2b79b35303af54fcb5baaf7c68", Algorithm: common.SHA1}},
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_1.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{Value: "bc417a575ceae93435bcb7bfd382ac28cbdaa8b5", Algorithm: common.SHA1}},
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_1.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{Value: "bc417a575ceae93435bcb7bfd382ac28cbdaa8b5", Algorithm: common.SHA1}},
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_1.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{Value: "ba226db943bbbf455da77afab6f16dbab156d000", Algorithm: common.SHA1}},
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_1.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{Value: "b6e0ec7d085c5699b46f6f8d425413702652874d", Algorithm: common.SHA1}},
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_1.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{Value: "ba226db943bbbf455da77afab6f16dbab156d000", Algorithm: common.SHA1}},
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_1.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{Value: "b6e0ec7d085c5699b46f6f8d425413702652874d", Algorithm: common.SHA1}},
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_1.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_1.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_1.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_1.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_1(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // now, create the LicenseDiff structured results from the pairs
+ diffResults, err := MakeResults(diffMap)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are the expected lengths
+ if len(diffResults.InBothChanged) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothChanged))
+ }
+ if len(diffResults.InBothSame) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothSame))
+ }
+ if len(diffResults.InFirstOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InFirstOnly))
+ }
+ if len(diffResults.InSecondOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InSecondOnly))
+ }
+
+ // check each filename is present where it belongs, and check license(s)
+
+ // in both and different license: f4 and f6
+ // filename will map to a LicensePair
+ check4, ok := diffResults.InBothChanged["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check4")
+ }
+ if check4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, check4.First)
+ }
+ if check4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, check4.Second)
+ }
+ check6, ok := diffResults.InBothChanged["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check6")
+ }
+ if check6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, check6.First)
+ }
+ if check6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, check6.Second)
+ }
+
+ // in both and same license: f1 and f5
+ // filename will map to a string
+ check1, ok := diffResults.InBothSame["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check1")
+ }
+ if check1 != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, check1)
+ }
+ check5, ok := diffResults.InBothSame["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check5")
+ }
+ if check5 != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, check5)
+ }
+ if check5 != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, check5)
+ }
+
+ // in first only: f2
+ // filename will map to a string
+ check2, ok := diffResults.InFirstOnly["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check2")
+ }
+ if check2 != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, check2)
+ }
+
+ // in second only: f3
+ // filename will map to a string
+ check3, ok := diffResults.InSecondOnly["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check3")
+ }
+ if check3 != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, check2)
+ }
+
+}
+
+// ===== 2.2 License diff top-level function tests =====
+func Test2_2DifferCanCreateDiffPairs(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_2.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_2.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_2.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_2.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_2.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_2.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_2.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_2.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_2.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_2.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_2.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_2.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_2.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_2(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are what we expect
+ // there should be 6 entries, one for each unique filename
+ if len(diffMap) != 6 {
+ t.Fatalf("Expected %d, got %d", 6, len(diffMap))
+ }
+
+ // check each filename is present, and check its pair
+ // pair 1 -- same in both
+ pair1, ok := diffMap["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair1")
+ }
+ if pair1.First != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, pair1.First)
+ }
+ if pair1.Second != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair1.Second)
+ }
+
+ // pair 2 -- only in first
+ pair2, ok := diffMap["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair2")
+ }
+ if pair2.First != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair2.First)
+ }
+ if pair2.Second != "" {
+ t.Errorf("Expected %s, got %s", "", pair2.Second)
+ }
+
+ // pair 3 -- only in second
+ pair3, ok := diffMap["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair3")
+ }
+ if pair3.First != "" {
+ t.Errorf("Expected %s, got %s", "", pair3.First)
+ }
+ if pair3.Second != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, pair3.Second)
+ }
+
+ // pair 4 -- in both but different license
+ pair4, ok := diffMap["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair4")
+ }
+ if pair4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, pair4.First)
+ }
+ if pair4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, pair4.Second)
+ }
+
+ // pair 5 -- in both but different hash, same license
+ pair5, ok := diffMap["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair5")
+ }
+ if pair5.First != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, pair5.First)
+ }
+ if pair5.Second != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, pair5.Second)
+ }
+
+ // pair 6 -- in both but different hash, different license
+ pair6, ok := diffMap["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair6")
+ }
+ if pair6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, pair6.First)
+ }
+ if pair6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, pair6.Second)
+ }
+}
+
+func Test2_2DifferCanCreateDiffStructuredResults(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_2.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_2.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_2.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_2.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_2.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_2.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_2.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_2.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_2.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_2.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_2.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_2.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_2.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_2(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // now, create the LicenseDiff structured results from the pairs
+ diffResults, err := MakeResults(diffMap)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are the expected lengths
+ if len(diffResults.InBothChanged) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothChanged))
+ }
+ if len(diffResults.InBothSame) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothSame))
+ }
+ if len(diffResults.InFirstOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InFirstOnly))
+ }
+ if len(diffResults.InSecondOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InSecondOnly))
+ }
+
+ // check each filename is present where it belongs, and check license(s)
+
+ // in both and different license: f4 and f6
+ // filename will map to a LicensePair
+ check4, ok := diffResults.InBothChanged["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check4")
+ }
+ if check4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, check4.First)
+ }
+ if check4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, check4.Second)
+ }
+ check6, ok := diffResults.InBothChanged["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check6")
+ }
+ if check6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, check6.First)
+ }
+ if check6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, check6.Second)
+ }
+
+ // in both and same license: f1 and f5
+ // filename will map to a string
+ check1, ok := diffResults.InBothSame["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check1")
+ }
+ if check1 != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, check1)
+ }
+ check5, ok := diffResults.InBothSame["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check5")
+ }
+ if check5 != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, check5)
+ }
+ if check5 != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, check5)
+ }
+
+ // in first only: f2
+ // filename will map to a string
+ check2, ok := diffResults.InFirstOnly["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check2")
+ }
+ if check2 != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, check2)
+ }
+
+ // in second only: f3
+ // filename will map to a string
+ check3, ok := diffResults.InSecondOnly["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check3")
+ }
+ if check3 != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, check2)
+ }
+
+}
+
+// ===== 2.3 License diff top-level function tests =====
+func Test2_3DifferCanCreateDiffPairs(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_3.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_3.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_3.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_3.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_3(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are what we expect
+ // there should be 6 entries, one for each unique filename
+ if len(diffMap) != 6 {
+ t.Fatalf("Expected %d, got %d", 6, len(diffMap))
+ }
+
+ // check each filename is present, and check its pair
+ // pair 1 -- same in both
+ pair1, ok := diffMap["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair1")
+ }
+ if pair1.First != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, pair1.First)
+ }
+ if pair1.Second != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair1.Second)
+ }
+
+ // pair 2 -- only in first
+ pair2, ok := diffMap["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair2")
+ }
+ if pair2.First != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, pair2.First)
+ }
+ if pair2.Second != "" {
+ t.Errorf("Expected %s, got %s", "", pair2.Second)
+ }
+
+ // pair 3 -- only in second
+ pair3, ok := diffMap["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair3")
+ }
+ if pair3.First != "" {
+ t.Errorf("Expected %s, got %s", "", pair3.First)
+ }
+ if pair3.Second != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, pair3.Second)
+ }
+
+ // pair 4 -- in both but different license
+ pair4, ok := diffMap["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair4")
+ }
+ if pair4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, pair4.First)
+ }
+ if pair4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, pair4.Second)
+ }
+
+ // pair 5 -- in both but different hash, same license
+ pair5, ok := diffMap["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair5")
+ }
+ if pair5.First != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, pair5.First)
+ }
+ if pair5.Second != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, pair5.Second)
+ }
+
+ // pair 6 -- in both but different hash, different license
+ pair6, ok := diffMap["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get pair6")
+ }
+ if pair6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, pair6.First)
+ }
+ if pair6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, pair6.Second)
+ }
+}
+
+func Test2_3DifferCanCreateDiffStructuredResults(t *testing.T) {
+ // create files to be used in diff
+ // f1 will be identical in both
+ f1 := &v2_3.File{
+ FileName: "/project/file1.txt",
+ FileSPDXIdentifier: common.ElementID("File561"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "LicenseRef-We-will-ignore-LicenseInfoInFiles",
+ },
+ FileCopyrightText: "We'll ignore copyright values",
+ }
+
+ // f2 will only appear in the first Package
+ f2 := &v2_3.File{
+ FileName: "/project/file2.txt",
+ FileSPDXIdentifier: common.ElementID("File562"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "GPL-2.0-or-later",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f3 will only appear in the second Package
+ f3 := &v2_3.File{
+ FileName: "/project/file3.txt",
+ FileSPDXIdentifier: common.ElementID("File563"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MPL-2.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f4_1 and f4_2 will appear in first and second,
+ // with same name, same hash and different license
+ f4_1 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f4_2 := &v2_3.File{
+ FileName: "/project/file4.txt",
+ FileSPDXIdentifier: common.ElementID("File564"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Apache-2.0 AND MIT",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f5_1 and f5_2 will appear in first and second,
+ // with same name, different hash and same license
+ f5_1 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f5_2 := &v2_3.File{
+ FileName: "/project/file5.txt",
+ FileSPDXIdentifier: common.ElementID("File565"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+
+ LicenseConcluded: "BSD-3-Clause",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // f6_1 and f6_2 will appear in first and second,
+ // with same name, different hash and different license
+ f6_1 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "CC0-1.0",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+ f6_2 := &v2_3.File{
+ FileName: "/project/file6.txt",
+ FileSPDXIdentifier: common.ElementID("File566"),
+ Checksums: []common.Checksum{{
+ Algorithm: common.SHA1,
+ Value: "6c92dc8bc462b6889d9b1c0bc16c54d19a2cbdd3",
+ },
+ },
+ LicenseConcluded: "Unlicense",
+ LicenseInfoInFiles: []string{
+ "NOASSERTION",
+ },
+ FileCopyrightText: "NOASSERTION",
+ }
+
+ // create Packages
+ p1 := &v2_3.Package{
+ PackageName: "p1",
+ PackageSPDXIdentifier: common.ElementID("p1"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "abc123abc123"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f2,
+ f4_1,
+ f5_1,
+ f6_1,
+ },
+ }
+ p2 := &v2_3.Package{
+ PackageName: "p2",
+ PackageSPDXIdentifier: common.ElementID("p2"),
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: true,
+ // fake the verification code for present purposes
+ PackageVerificationCode: &common.PackageVerificationCode{Value: "def456def456"},
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseInfoFromFiles: []string{
+ "NOASSERTION",
+ },
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageCopyrightText: "NOASSERTION",
+ Files: []*v2_3.File{
+ f1,
+ f3,
+ f4_2,
+ f5_2,
+ f6_2,
+ },
+ }
+
+ // run the diff between the two packages
+ diffMap, err := MakePairs2_3(p1, p2)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // now, create the LicenseDiff structured results from the pairs
+ diffResults, err := MakeResults(diffMap)
+ if err != nil {
+ t.Fatalf("Expected nil error, got %v", err)
+ }
+
+ // check that the diff results are the expected lengths
+ if len(diffResults.InBothChanged) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothChanged))
+ }
+ if len(diffResults.InBothSame) != 2 {
+ t.Fatalf("Expected %d, got %d", 2, len(diffResults.InBothSame))
+ }
+ if len(diffResults.InFirstOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InFirstOnly))
+ }
+ if len(diffResults.InSecondOnly) != 1 {
+ t.Fatalf("Expected %d, got %d", 1, len(diffResults.InSecondOnly))
+ }
+
+ // check each filename is present where it belongs, and check license(s)
+
+ // in both and different license: f4 and f6
+ // filename will map to a LicensePair
+ check4, ok := diffResults.InBothChanged["/project/file4.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check4")
+ }
+ if check4.First != f4_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_1.LicenseConcluded, check4.First)
+ }
+ if check4.Second != f4_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f4_2.LicenseConcluded, check4.Second)
+ }
+ check6, ok := diffResults.InBothChanged["/project/file6.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check6")
+ }
+ if check6.First != f6_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_1.LicenseConcluded, check6.First)
+ }
+ if check6.Second != f6_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f6_2.LicenseConcluded, check6.Second)
+ }
+
+ // in both and same license: f1 and f5
+ // filename will map to a string
+ check1, ok := diffResults.InBothSame["/project/file1.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check1")
+ }
+ if check1 != f1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f1.LicenseConcluded, check1)
+ }
+ check5, ok := diffResults.InBothSame["/project/file5.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check5")
+ }
+ if check5 != f5_1.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_1.LicenseConcluded, check5)
+ }
+ if check5 != f5_2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f5_2.LicenseConcluded, check5)
+ }
+
+ // in first only: f2
+ // filename will map to a string
+ check2, ok := diffResults.InFirstOnly["/project/file2.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check2")
+ }
+ if check2 != f2.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f2.LicenseConcluded, check2)
+ }
+
+ // in second only: f3
+ // filename will map to a string
+ check3, ok := diffResults.InSecondOnly["/project/file3.txt"]
+ if !ok {
+ t.Fatalf("Couldn't get check3")
+ }
+ if check3 != f3.LicenseConcluded {
+ t.Errorf("Expected %s, got %s", f3.LicenseConcluded, check2)
+ }
+
+}
diff --git a/rdfloader/parser2v2/constants.go b/rdfloader/parser2v2/constants.go
new file mode 100644
index 0000000..71a3e33
--- /dev/null
+++ b/rdfloader/parser2v2/constants.go
@@ -0,0 +1,264 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import "github.com/spdx/gordf/rdfloader/parser"
+
+var (
+ // NAMESPACES
+ NS_SPDX = "http://spdx.org/rdf/terms#"
+ NS_RDFS = "http://www.w3.org/2000/01/rdf-schema#"
+ NS_RDF = parser.RDFNS
+ NS_PTR = "http://www.w3.org/2009/pointers#"
+ NS_DOAP = "http://usefulinc.com/ns/doap#"
+
+ // SPDX properties
+ SPDX_SPEC_VERSION = NS_SPDX + "specVersion"
+ SPDX_DATA_LICENSE = NS_SPDX + "dataLicense"
+ SPDX_NAME = NS_SPDX + "name"
+ SPDX_EXTERNAL_DOCUMENT_REF = NS_SPDX + "externalDocumentRef"
+ SPDX_LICENSE_LIST_VERSION = NS_SPDX + "licenseListVersion"
+ SPDX_CREATOR = NS_SPDX + "creator"
+ SPDX_CREATED = NS_SPDX + "created"
+ SPDX_REVIEWED = NS_SPDX + "reviewed"
+ SPDX_DESCRIBES_PACKAGE = NS_SPDX + "describesPackage"
+ SPDX_HAS_EXTRACTED_LICENSING_INFO = NS_SPDX + "hasExtractedLicensingInfo"
+ SPDX_RELATIONSHIP = NS_SPDX + "relationship"
+ SPDX_ANNOTATION = NS_SPDX + "annotation"
+ SPDX_COMMENT = NS_SPDX + "comment"
+ SPDX_CREATION_INFO = NS_SPDX + "creationInfo"
+ SPDX_CHECKSUM_ALGORITHM_SHA1 = NS_SPDX + "checksumAlgorithm_sha1"
+ SPDX_CHECKSUM_ALGORITHM_SHA256 = NS_SPDX + "checksumAlgorithm_sha256"
+ SPDX_CHECKSUM_ALGORITHM_MD5 = NS_SPDX + "checksumAlgorithm_md5"
+ SPDX_EXTERNAL_DOCUMENT_ID = NS_SPDX + "externalDocumentId"
+ SPDX_SPDX_DOCUMENT = NS_SPDX + "spdxDocument"
+ SPDX_SPDX_DOCUMENT_CAPITALIZED = NS_SPDX + "SpdxDocument"
+ SPDX_CHECKSUM = NS_SPDX + "checksum"
+ SPDX_CHECKSUM_CAPITALIZED = NS_SPDX + "Checksum"
+ SPDX_ANNOTATION_TYPE = NS_SPDX + "annotationType"
+ SPDX_ANNOTATION_TYPE_OTHER = NS_SPDX + "annotationType_other"
+ SPDX_ANNOTATION_TYPE_REVIEW = NS_SPDX + "annotationType_review"
+ SPDX_LICENSE_INFO_IN_FILE = NS_SPDX + "licenseInfoInFile"
+ SPDX_LICENSE_CONCLUDED = NS_SPDX + "licenseConcluded"
+ SPDX_LICENSE_COMMENTS = NS_SPDX + "licenseComments"
+ SPDX_COPYRIGHT_TEXT = NS_SPDX + "copyrightText"
+ SPDX_ARTIFACT_OF = NS_SPDX + "artifactOf"
+ SPDX_NOTICE_TEXT = NS_SPDX + "noticeText"
+ SPDX_FILE_CONTRIBUTOR = NS_SPDX + "fileContributor"
+ SPDX_FILE_DEPENDENCY = NS_SPDX + "fileDependency"
+ SPDX_FILE_TYPE = NS_SPDX + "fileType"
+ SPDX_FILE_NAME = NS_SPDX + "fileName"
+ SPDX_EXTRACTED_TEXT = NS_SPDX + "extractedText"
+ SPDX_LICENSE_ID = NS_SPDX + "licenseId"
+ SPDX_FILE = NS_SPDX + "File"
+ SPDX_PACKAGE = NS_SPDX + "Package"
+ SPDX_SPDX_ELEMENT = NS_SPDX + "SpdxElement"
+ SPDX_VERSION_INFO = NS_SPDX + "versionInfo"
+ SPDX_PACKAGE_FILE_NAME = NS_SPDX + "packageFileName"
+ SPDX_SUPPLIER = NS_SPDX + "supplier"
+ SPDX_ORIGINATOR = NS_SPDX + "originator"
+ SPDX_DOWNLOAD_LOCATION = NS_SPDX + "downloadLocation"
+ SPDX_FILES_ANALYZED = NS_SPDX + "filesAnalyzed"
+ SPDX_PACKAGE_VERIFICATION_CODE = NS_SPDX + "packageVerificationCode"
+ SPDX_SOURCE_INFO = NS_SPDX + "sourceInfo"
+ SPDX_LICENSE_INFO_FROM_FILES = NS_SPDX + "licenseInfoFromFiles"
+ SPDX_LICENSE_DECLARED = NS_SPDX + "licenseDeclared"
+ SPDX_SUMMARY = NS_SPDX + "summary"
+ SPDX_DESCRIPTION = NS_SPDX + "description"
+ SPDX_EXTERNAL_REF = NS_SPDX + "externalRef"
+ SPDX_HAS_FILE = NS_SPDX + "hasFile"
+ SPDX_ATTRIBUTION_TEXT = NS_SPDX + "attributionText"
+ SPDX_PACKAGE_VERIFICATION_CODE_VALUE = NS_SPDX + "packageVerificationCodeValue"
+ SPDX_PACKAGE_VERIFICATION_CODE_EXCLUDED_FILE = NS_SPDX + "packageVerificationCodeExcludedFile"
+ SPDX_RELATED_SPDX_ELEMENT = NS_SPDX + "relatedSpdxElement"
+ SPDX_RELATIONSHIP_TYPE = NS_SPDX + "relationshipType"
+ SPDX_SNIPPET_FROM_FILE = NS_SPDX + "snippetFromFile"
+ SPDX_LICENSE_INFO_IN_SNIPPET = NS_SPDX + "licenseInfoInSnippet"
+ SPDX_RANGE = NS_SPDX + "range"
+ SPDX_REVIEWER = NS_SPDX + "reviewer"
+ SPDX_REVIEW_DATE = NS_SPDX + "reviewDate"
+ SPDX_SNIPPET = NS_SPDX + "Snippet"
+ SPDX_ALGORITHM = NS_SPDX + "algorithm"
+ SPDX_CHECKSUM_VALUE = NS_SPDX + "checksumValue"
+ SPDX_REFERENCE_CATEGORY = NS_SPDX + "referenceCategory"
+ SPDX_REFERENCE_CATEGORY_PACKAGE_MANAGER = NS_SPDX + "referenceCategory_packageManager"
+ SPDX_REFERENCE_CATEGORY_SECURITY = NS_SPDX + "referenceCategory_security"
+ SPDX_REFERENCE_CATEGORY_OTHER = NS_SPDX + "referenceCategory_other"
+
+ SPDX_REFERENCE_TYPE = NS_SPDX + "referenceType"
+ SPDX_REFERENCE_LOCATOR = NS_SPDX + "referenceLocator"
+ SPDX_ANNOTATION_DATE = NS_SPDX + "annotationDate"
+ SPDX_ANNOTATOR = NS_SPDX + "annotator"
+ SPDX_MEMBER = NS_SPDX + "member"
+ SPDX_DISJUNCTIVE_LICENSE_SET = NS_SPDX + "DisjunctiveLicenseSet"
+ SPDX_CONJUNCTIVE_LICENSE_SET = NS_SPDX + "ConjunctiveLicenseSet"
+ SPDX_EXTRACTED_LICENSING_INFO = NS_SPDX + "ExtractedLicensingInfo"
+ SPDX_SIMPLE_LICENSING_INFO = NS_SPDX + "SimpleLicensingInfo"
+ SPDX_NONE_CAPS = NS_SPDX + "NONE"
+ SPDX_NOASSERTION_CAPS = NS_SPDX + "NOASSERTION"
+ SPDX_NONE_SMALL = NS_SPDX + "none"
+ SPDX_NOASSERTION_SMALL = NS_SPDX + "noassertion"
+ SPDX_LICENSE = NS_SPDX + "License"
+ SPDX_LISTED_LICENSE = NS_SPDX + "ListedLicense"
+ SPDX_EXAMPLE = NS_SPDX + "example"
+ SPDX_IS_OSI_APPROVED = NS_SPDX + "isOsiApproved"
+ SPDX_STANDARD_LICENSE_TEMPLATE = NS_SPDX + "standardLicenseTemplate"
+ SPDX_IS_DEPRECATED_LICENSE_ID = NS_SPDX + "isDeprecatedLicenseId"
+ SPDX_IS_FSF_LIBRE = NS_SPDX + "isFsfLibre"
+ SPDX_LICENSE_TEXT = NS_SPDX + "licenseText"
+ SPDX_STANDARD_LICENSE_HEADER = NS_SPDX + "standardLicenseHeader"
+ SPDX_LICENSE_EXCEPTION_ID = NS_SPDX + "licenseExceptionId"
+ SPDX_LICENSE_EXCEPTION_TEXT = NS_SPDX + "licenseExceptionText"
+ SPDX_LICENSE_EXCEPTION = NS_SPDX + "licenseException"
+ SPDX_WITH_EXCEPTION_OPERATOR = NS_SPDX + "WithExceptionOperator"
+ SPDX_OR_LATER_OPERATOR = NS_SPDX + "OrLaterOperator"
+ SPDX_STANDARD_LICENSE_HEADER_TEMPLATE = NS_SPDX + "standardLicenseHeaderTemplate"
+
+ // RDFS properties
+ RDFS_COMMENT = NS_RDFS + "comment"
+ RDFS_SEE_ALSO = NS_RDFS + "seeAlso"
+
+ // RDF properties
+ RDF_TYPE = NS_RDF + "type"
+
+ // DOAP properties
+ DOAP_HOMEPAGE = NS_DOAP + "homepage"
+ DOAP_NAME = NS_DOAP + "name"
+
+ // PTR properties
+ PTR_START_END_POINTER = NS_PTR + "StartEndPointer"
+ PTR_START_POINTER = NS_PTR + "startPointer"
+ PTR_BYTE_OFFSET_POINTER = NS_PTR + "ByteOffsetPointer"
+ PTR_LINE_CHAR_POINTER = NS_PTR + "LineCharPointer"
+ PTR_REFERENCE = NS_PTR + "reference"
+ PTR_OFFSET = NS_PTR + "offset"
+ PTR_LINE_NUMBER = NS_PTR + "lineNumber"
+ PTR_END_POINTER = NS_PTR + "endPointer"
+
+ // prefixes
+ PREFIX_RELATIONSHIP_TYPE = "relationshipType_"
+)
+
+func AllRelationshipTypes() []string {
+ return []string{
+ "amendment", "ancestorOf", "buildDependencyOf", "buildToolOf",
+ "containedBy", "contains", "copyOf", "dataFile", "dataFileOf",
+ "dependencyManifestOf", "dependencyOf", "dependsOn", "descendantOf",
+ "describedBy", "describes", "devDependencyOf", "devToolOf",
+ "distributionArtifact", "documentation", "dynamicLink", "exampleOf",
+ "expandedFromArchive", "fileAdded", "fileDeleted", "fileModified",
+ "generatedFrom", "generates", "hasPrerequisite", "metafileOf",
+ "optionalComponentOf", "optionalDependencyOf", "other", "packageOf",
+ "patchApplied", "patchFor", "prerequisiteFor", "providedDependencyOf",
+ "runtimeDependencyOf", "staticLink", "testDependencyOf", "testOf",
+ "testToolOf", "testcaseOf", "variantOf",
+ }
+}
+
+func AllStandardLicenseIDS() []string {
+ return []string{
+ "0BSD", "389-exception", "AAL", "Abstyles", "Adobe-2006", "Adobe-Glyph",
+ "ADSL", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "Afmparse",
+ "AGPL-1.0-only", "AGPL-1.0-or-later", "AGPL-1.0", "AGPL-3.0-only",
+ "AGPL-3.0-or-later", "AGPL-3.0", "Aladdin", "AMDPLPA", "AML", "AMPAS",
+ "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APAFML", "APL-1.0",
+ "APSL-1.0", "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0-cl8",
+ "Artistic-1.0-Perl", "Artistic-1.0", "Artistic-2.0", "",
+ "Autoconf-exception-2.0", "Autoconf-exception-3.0", "Bahyph", "Barr",
+ "Beerware", "Bison-exception-2.2", "BitTorrent-1.0", "BitTorrent-1.1",
+ "blessing", "BlueOak-1.0.0", "Bootloader-exception", "Borceux", "BSD-1-Clause",
+ "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", "BSD-2-Clause-Patent",
+ "BSD-2-Clause-Views", "BSD-2-Clause", "BSD-3-Clause-Attribution",
+ "BSD-3-Clause-Clear", "BSD-3-Clause-LBNL",
+ "BSD-3-Clause-No-Nuclear-License-2014", "BSD-3-Clause-No-Nuclear-License",
+ "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause-Open-MPI", "BSD-3-Clause",
+ "BSD-4-Clause-UC", "BSD-4-Clause", "BSD-Protection", "BSD-Source-Code",
+ "BSL-1.0", "bzip2-1.0.5", "bzip2-1.0.6", "CAL-1.0-Combined-Work-Exception",
+ "CAL-1.0", "Caldera", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5",
+ "CC-BY-3.0-AT", "CC-BY-3.0", "CC-BY-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0",
+ "CC-BY-NC-2.5", "CC-BY-NC-3.0", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0",
+ "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0-IGO", "CC-BY-NC-ND-3.0",
+ "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5",
+ "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0",
+ "CC-BY-ND-2.5", "CC-BY-ND-3.0", "CC-BY-ND-4.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0",
+ "CC-BY-SA-2.5", "CC-BY-SA-3.0-AT", "CC-BY-SA-3.0", "CC-BY-SA-4.0", "CC-PDDC",
+ "CC0-1.0", "CDDL-1.0", "CDDL-1.1", "CDLA-Permissive-1.0", "CDLA-Sharing-1.0",
+ "CECILL-1.0", "CECILL-1.1", "CECILL-2.0", "CECILL-2.1", "CECILL-B", "CECILL-C",
+ "CERN-OHL-1.1", "CERN-OHL-1.2", "CERN-OHL-P-2.0", "CERN-OHL-S-2.0",
+ "CERN-OHL-W-2.0", "ClArtistic", "Classpath-exception-2.0",
+ "CLISP-exception-2.0", "CNRI-Jython", "CNRI-Python-GPL-Compatible",
+ "CNRI-Python", "Condor-1.1", "copyleft-next-0.3.0", "copyleft-next-0.3.1",
+ "CPAL-1.0", "CPL-1.0", "CPOL-1.02", "Crossword", "CrystalStacker",
+ "CUA-OPL-1.0", "Cube", "curl", "D-FSL-1.0", "diffmark",
+ "DigiRule-FOSS-exception", "DOC", "Dotseqn", "DSDP", "dvipdfm", "ECL-1.0",
+ "ECL-2.0", "eCos-2.0", "eCos-exception-2.0", "EFL-1.0", "EFL-2.0", "eGenix",
+ "Entessa", "EPICS", "EPL-1.0", "EPL-2.0", "ErlPL-1.1", "etalab-2.0",
+ "EUDatagrid", "EUPL-1.0", "EUPL-1.1", "EUPL-1.2", "Eurosym", "Fair",
+ "Fawkes-Runtime-exception", "FLTK-exception", "Font-exception-2.0",
+ "Frameworx-1.0", "FreeImage", "freertos-exception-2.0", "FSFAP", "FSFUL",
+ "FSFULLR", "FTL", "GCC-exception-2.0", "GCC-exception-3.1",
+ "GFDL-1.1-invariants-only", "GFDL-1.1-invariants-or-later",
+ "GFDL-1.1-no-invariants-only", "GFDL-1.1-no-invariants-or-later",
+ "GFDL-1.1-only", "GFDL-1.1-or-later", "GFDL-1.1", "GFDL-1.2-invariants-only",
+ "GFDL-1.2-invariants-or-later", "GFDL-1.2-no-invariants-only",
+ "GFDL-1.2-no-invariants-or-later", "GFDL-1.2-only", "GFDL-1.2-or-later",
+ "GFDL-1.2", "GFDL-1.3-invariants-only", "GFDL-1.3-invariants-or-later",
+ "GFDL-1.3-no-invariants-only", "GFDL-1.3-no-invariants-or-later",
+ "GFDL-1.3-only", "GFDL-1.3-or-later", "GFDL-1.3", "Giftware", "GL2PS", "Glide",
+ "Glulxe", "GLWTPL", "gnu-javamail-exception", "gnuplot", "GPL-1.0+",
+ "GPL-1.0-only", "GPL-1.0-or-later", "GPL-1.0", "GPL-2.0+", "GPL-2.0-only",
+ "GPL-2.0-or-later", "GPL-2.0-with-autoconf-exception",
+ "GPL-2.0-with-bison-exception", "GPL-2.0-with-classpath-exception",
+ "GPL-2.0-with-font-exception", "GPL-2.0-with-GCC-exception", "GPL-2.0",
+ "GPL-3.0+", "GPL-3.0-linking-exception", "GPL-3.0-linking-source-exception",
+ "GPL-3.0-only", "GPL-3.0-or-later", "GPL-3.0-with-autoconf-exception",
+ "GPL-3.0-with-GCC-exception", "GPL-3.0", "GPL-CC-1.0", "gSOAP-1.3b",
+ "HaskellReport", "Hippocratic-2.1", "HPND-sell-variant", "HPND",
+ "i2p-gpl-java-exception", "IBM-pibs", "ICU", "IJG", "ImageMagick", "iMatix",
+ "Imlib2", "Info-ZIP", "Intel-ACPI", "Intel", "Interbase-1.0", "IPA", "IPL-1.0",
+ "ISC", "JasPer-2.0", "JPNIC", "JSON", "LAL-1.2", "LAL-1.3", "Latex2e",
+ "Leptonica", "LGPL-2.0+", "LGPL-2.0-only", "LGPL-2.0-or-later", "LGPL-2.0",
+ "LGPL-2.1+", "LGPL-2.1-only", "LGPL-2.1-or-later", "LGPL-2.1", "LGPL-3.0+",
+ "LGPL-3.0-linking-exception", "LGPL-3.0-only", "LGPL-3.0-or-later", "LGPL-3.0",
+ "LGPLLR", "libpng-2.0", "Libpng", "libselinux-1.0", "libtiff",
+ "Libtool-exception", "licenses", "LiLiQ-P-1.1", "LiLiQ-R-1.1",
+ "LiLiQ-Rplus-1.1", "Linux-OpenIB", "Linux-syscall-note", "LLVM-exception",
+ "LPL-1.0", "LPL-1.02", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3a",
+ "LPPL-1.3c", "LZMA-exception", "MakeIndex", "mif-exception", "MirOS", "MIT-0",
+ "MIT-advertising", "MIT-CMU", "MIT-enna", "MIT-feh", "MIT", "MITNFA",
+ "Motosoto", "mpich2", "MPL-1.0", "MPL-1.1", "MPL-2.0-no-copyleft-exception",
+ "MPL-2.0", "MS-PL", "MS-RL", "MTLL", "MulanPSL-1.0", "MulanPSL-2.0", "Multics",
+ "Mup", "NASA-1.3", "Naumen", "NBPL-1.0", "NCGL-UK-2.0", "NCSA", "Net-SNMP",
+ "NetCDF", "Newsletr", "NGPL", "NIST-PD-fallback", "NIST-PD", "NLOD-1.0",
+ "NLPL", "Nokia-Qt-exception-1.1", "Nokia", "NOSL", "Noweb", "NPL-1.0",
+ "NPL-1.1", "NPOSL-3.0", "NRL", "NTP-0", "NTP", "Nunit", "O-UDA-1.0",
+ "OCaml-LGPL-linking-exception", "OCCT-exception-1.0", "OCCT-PL", "OCLC-2.0",
+ "ODbL-1.0", "ODC-By-1.0", "OFL-1.0-no-RFN", "OFL-1.0-RFN", "OFL-1.0",
+ "OFL-1.1-no-RFN", "OFL-1.1-RFN", "OFL-1.1", "OGC-1.0", "OGL-Canada-2.0",
+ "OGL-UK-1.0", "OGL-UK-2.0", "OGL-UK-3.0", "OGTSL", "OLDAP-1.1", "OLDAP-1.2",
+ "OLDAP-1.3", "OLDAP-1.4", "OLDAP-2.0.1", "OLDAP-2.0", "OLDAP-2.1",
+ "OLDAP-2.2.1", "OLDAP-2.2.2", "OLDAP-2.2", "OLDAP-2.3", "OLDAP-2.4",
+ "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OLDAP-2.8", "OML", "",
+ "OpenJDK-assembly-exception-1.0", "OpenSSL", "openvpn-openssl-exception",
+ "OPL-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1",
+ "OSL-3.0", "Parity-6.0.0", "Parity-7.0.0", "PDDL-1.0", "PHP-3.0", "PHP-3.01",
+ "Plexus", "PolyForm-Noncommercial-1.0.0", "PolyForm-Small-Business-1.0.0",
+ "PostgreSQL", "PS-or-PDF-font-exception-20170817", "PSF-2.0", "psfrag",
+ "psutils", "Python-2.0", "Qhull", "QPL-1.0", "Qt-GPL-exception-1.0",
+ "Qt-LGPL-exception-1.1", "Qwt-exception-1.0", "Rdisc", "RHeCos-1.1", "RPL-1.1",
+ "RPL-1.5", "RPSL-1.0", "RSA-MD", "RSCPL", "Ruby", "SAX-PD", "Saxpath", "SCEA",
+ "Sendmail-8.23", "Sendmail", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", "SHL-0.5",
+ "SHL-0.51", "SHL-2.0", "SHL-2.1", "SimPL-2.0", "SISSL-1.2", "SISSL",
+ "Sleepycat", "SMLNJ", "SMPPL", "SNIA", "Spencer-86", "Spencer-94",
+ "Spencer-99", "SPL-1.0", "SSH-OpenSSH", "SSH-short", "SSPL-1.0",
+ "StandardML-NJ", "SugarCRM-1.1.3", "Swift-exception", "SWL", "TAPR-OHL-1.0",
+ "TCL", "TCP-wrappers", "TMate", "TORQUE-1.1", "TOSL", "TU-Berlin-1.0",
+ "TU-Berlin-2.0", "u-boot-exception-2.0", "UCL-1.0", "Unicode-DFS-2015",
+ "Unicode-DFS-2016", "Unicode-TOU", "Universal-FOSS-exception-1.0", "Unlicense",
+ "UPL-1.0", "Vim", "VOSTROM", "VSL-1.0", "W3C-19980720", "W3C-20150513", "W3C",
+ "Watcom-1.0", "Wsuipa", "WTFPL", "WxWindows-exception-3.1", "wxWindows", "X11",
+ "Xerox", "XFree86-1.1", "xinetd", "Xnet", "xpp", "XSkat", "YPL-1.0", "YPL-1.1",
+ "Zed", "Zend-2.0", "Zimbra-1.3", "Zimbra-1.4", "zlib-acknowledgement", "Zlib",
+ "ZPL-1.1", "ZPL-2.0", "ZPL-2.1",
+ }
+}
diff --git a/rdfloader/parser2v2/license_utils.go b/rdfloader/parser2v2/license_utils.go
new file mode 100644
index 0000000..41e8870
--- /dev/null
+++ b/rdfloader/parser2v2/license_utils.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+/* util methods for licenses and checksums below:*/
+
+// Given the license URI, returns the name of the license defined
+// in the last part of the uri.
+// This function is susceptible to false-positives.
+func getLicenseStringFromURI(uri string) string {
+ licenseEnd := strings.TrimSpace(getLastPartOfURI(uri))
+ lower := strings.ToLower(licenseEnd)
+ if lower == "none" || lower == "noassertion" {
+ return strings.ToUpper(licenseEnd)
+ }
+ return licenseEnd
+}
+
+// returns the checksum algorithm and it's value
+// In the newer versions, these two strings will be bound to a single checksum struct
+// whose pointer will be returned.
+func (parser *rdfParser2_2) getChecksumFromNode(checksumNode *gordfParser.Node) (algorithm common.ChecksumAlgorithm, value string, err error) {
+ var checksumValue, checksumAlgorithm string
+ for _, checksumTriple := range parser.nodeToTriples(checksumNode) {
+ switch checksumTriple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_CHECKSUM_VALUE:
+ // cardinality: exactly 1
+ checksumValue = strings.TrimSpace(checksumTriple.Object.ID)
+ case SPDX_ALGORITHM:
+ // cardinality: exactly 1
+ checksumAlgorithm, err = getAlgorithmFromURI(checksumTriple.Object.ID)
+ if err != nil {
+ return
+ }
+ default:
+ err = fmt.Errorf("unknown predicate '%s' while parsing checksum node", checksumTriple.Predicate.ID)
+ return
+ }
+ }
+ return common.ChecksumAlgorithm(checksumAlgorithm), checksumValue, nil
+}
+
+func getAlgorithmFromURI(algorithmURI string) (checksumAlgorithm string, err error) {
+ fragment := getLastPartOfURI(algorithmURI)
+ if !strings.HasPrefix(fragment, "checksumAlgorithm_") {
+ return "", fmt.Errorf("checksum algorithm uri must begin with checksumAlgorithm_. found %s", fragment)
+ }
+ algorithm := strings.TrimPrefix(fragment, "checksumAlgorithm_")
+ algorithm = strings.ToLower(strings.TrimSpace(algorithm))
+ switch algorithm {
+ case "md2", "md4", "md5", "md6":
+ checksumAlgorithm = strings.ToUpper(algorithm)
+ case "sha1", "sha224", "sha256", "sha384", "sha512":
+ checksumAlgorithm = strings.ToUpper(algorithm)
+ default:
+ return "", fmt.Errorf("unknown checksum algorithm %s", algorithm)
+ }
+ return
+}
+
+// from a list of licenses, it returns a
+// list of string representation of those licenses.
+func mapLicensesToStrings(licences []AnyLicenseInfo) []string {
+ res := make([]string, len(licences), len(licences))
+ for i, lic := range licences {
+ res[i] = lic.ToLicenseString()
+ }
+ return res
+}
+
+/****** Type Functions ******/
+
+// TODO: should probably add brackets while linearizing a nested license.
+func (lic ConjunctiveLicenseSet) ToLicenseString() string {
+ return strings.Join(mapLicensesToStrings(lic.members), " AND ")
+}
+
+// TODO: should probably add brackets while linearizing a nested license.
+func (lic DisjunctiveLicenseSet) ToLicenseString() string {
+ return strings.Join(mapLicensesToStrings(lic.members), " OR ")
+}
+
+func (lic ExtractedLicensingInfo) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (operator OrLaterOperator) ToLicenseString() string {
+ return operator.member.ToLicenseString()
+}
+
+func (lic License) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (lic ListedLicense) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (lic WithExceptionOperator) ToLicenseString() string {
+ return lic.member.ToLicenseString()
+}
+
+func (lic SpecialLicense) ToLicenseString() string {
+ return string(lic.value)
+}
+
+func (lic SimpleLicensingInfo) ToLicenseString() string {
+ return lic.licenseID
+}
diff --git a/rdfloader/parser2v2/license_utils_test.go b/rdfloader/parser2v2/license_utils_test.go
new file mode 100644
index 0000000..7e20b5c
--- /dev/null
+++ b/rdfloader/parser2v2/license_utils_test.go
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "reflect"
+ "testing"
+)
+
+func Test_getLicenseStringFromURI(t *testing.T) {
+ // TestCase 1: NONE license
+ input := SPDX_NONE_CAPS
+ output := getLicenseStringFromURI(input)
+ expectedOutput := "NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+
+ // TestCase 2: NOASSERTION license
+ input = SPDX_NOASSERTION_SMALL
+ output = getLicenseStringFromURI(input)
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+
+ // TestCase 3: Other license
+ input = NS_SPDX + "LicenseRef-1"
+ output = getLicenseStringFromURI(input)
+ expectedOutput = "LicenseRef-1"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+}
+
+func Test_rdfParser2_2_getChecksumFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+ // TestCase 1: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ `)
+ checksumNode := parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getChecksumFromNode(checksumNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid checksum algorithm")
+ }
+
+ // TestCase 2: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ <spdx:invalidPredicate />
+ </spdx:Checksum>
+ `)
+ checksumNode = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getChecksumFromNode(checksumNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate")
+ }
+
+ // TestCase 3: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ `)
+ checksumNode = parser.gordfParserObj.Triples[0].Subject
+ algorithm, value, err := parser.getChecksumFromNode(checksumNode)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if algorithm != "SHA1" {
+ t.Errorf("expected checksum algorithm to be sha1, found %s", algorithm)
+ }
+ expectedValue := "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ if value != expectedValue {
+ t.Errorf("expected checksumValue to be %s, found %s", expectedValue, value)
+ }
+}
+
+func Test_rdfParser2_2_getAlgorithmFromURI(t *testing.T) {
+ var algorithmURI string
+ var err error
+
+ // TestCase 1: checksumAlgorithm uri doesn't start with checksumAlgorithm_
+ algorithmURI = NS_SPDX + "sha1"
+ _, err = getAlgorithmFromURI(algorithmURI)
+ if err == nil {
+ t.Errorf("should've raised an error for algorithmURI that doesn't start with checksumAlgorithm_")
+ }
+
+ // TestCase 2: unknown checksum algorithm
+ algorithmURI = NS_SPDX + "checksumAlgorithm_sha999"
+ _, err = getAlgorithmFromURI(algorithmURI)
+ if err == nil {
+ t.Errorf("should've raised an error for invalid algorithm")
+ }
+
+ // TestCase 3: valid input
+ algorithmURI = NS_SPDX + "checksumAlgorithm_sha256"
+ algorithm, err := getAlgorithmFromURI(algorithmURI)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if algorithm != "SHA256" {
+ t.Errorf("expected: SHA256, found: %s", algorithm)
+ }
+}
+
+func Test_mapLicensesToStrings(t *testing.T) {
+ // nothing much to test here.
+ // just a dummy dry run.
+ licenses := []AnyLicenseInfo{
+ SpecialLicense{
+ value: NONE,
+ },
+ SpecialLicense{
+ value: NOASSERTION,
+ },
+ }
+ licenseStrings := mapLicensesToStrings(licenses)
+ expectedLicenseStrings := []string{"NONE", "NOASSERTION"}
+ if !reflect.DeepEqual(licenseStrings, expectedLicenseStrings) {
+ t.Errorf("expected: %+v\nfound %+v", expectedLicenseStrings, licenseStrings)
+ }
+}
+
+func TestConjunctiveLicenseSet_ToLicenseString(t *testing.T) {
+ var lic ConjunctiveLicenseSet
+ var output, expectedOutput string
+
+ // TestCase 1: no license in the set
+ lic = ConjunctiveLicenseSet{
+ members: nil,
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = ""
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 2: single license in the set
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 3: more than one license in the set.
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ SpecialLicense{value: NONE},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION AND NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 4: nested conjunctive license.
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: "LicenseRef-1"},
+ SpecialLicense{value: NONE},
+ },
+ },
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION AND LicenseRef-1 AND NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+}
+
+func TestDisjunctiveLicenseSet_ToLicenseString(t *testing.T) {
+ var lic DisjunctiveLicenseSet
+ var output, expectedOutput string
+
+ // TestCase 1: no license in the set
+ lic = DisjunctiveLicenseSet{
+ members: nil,
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = ""
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 2: single license in the set
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 3: more than one license in the set.
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ SpecialLicense{value: NONE},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION OR NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 4: nested conjunctive license.
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: "LicenseRef-1"},
+ SpecialLicense{value: NONE},
+ },
+ },
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION OR LicenseRef-1 OR NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+}
+
+func TestExtractedLicensingInfo_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ extractedLicense := ExtractedLicensingInfo{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ extractedText: "extracted Text",
+ }
+ expectedOutput := "license"
+ output := extractedLicense.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestOrLaterOperator_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ orLater := OrLaterOperator{
+ member: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ }
+ expectedOutput := "license"
+ output := orLater.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ license := License{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ }
+ expectedOutput := "license"
+ output := license.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestListedLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ ll := ListedLicense{License{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ },
+ }
+ expectedOutput := "license"
+ output := ll.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestWithExceptionOperator_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ withException := WithExceptionOperator{
+ member: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ licenseException: LicenseException{},
+ }
+ expectedOutput := "license"
+ output := withException.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestSpecialLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ specialLicense := SpecialLicense{
+ value: "license",
+ }
+ expectedOutput := "license"
+ output := specialLicense.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestSimpleLicensingInfo_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ sli := SimpleLicensingInfo{
+ licenseID: "license",
+ }
+ expectedOutput := "license"
+ output := sli.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_annotation.go b/rdfloader/parser2v2/parse_annotation.go
new file mode 100644
index 0000000..a5801b0
--- /dev/null
+++ b/rdfloader/parser2v2/parse_annotation.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "errors"
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// creates a new instance of annotation and sets the annotation attributes
+// associated with the given node.
+// The newly created annotation is appended to the doc.
+func (parser *rdfParser2_2) parseAnnotationFromNode(node *gordfParser.Node) (err error) {
+ ann := &v2_2.Annotation{}
+ for _, subTriple := range parser.nodeToTriples(node) {
+ switch subTriple.Predicate.ID {
+ case SPDX_ANNOTATOR:
+ // cardinality: exactly 1
+ err = setAnnotatorFromString(subTriple.Object.ID, ann)
+ case SPDX_ANNOTATION_DATE:
+ // cardinality: exactly 1
+ ann.AnnotationDate = subTriple.Object.ID
+ case RDFS_COMMENT:
+ // cardinality: exactly 1
+ ann.AnnotationComment = subTriple.Object.ID
+ case SPDX_ANNOTATION_TYPE:
+ // cardinality: exactly 1
+ err = setAnnotationType(subTriple.Object.ID, ann)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ default:
+ err = fmt.Errorf("unknown predicate %s while parsing annotation", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return setAnnotationToParser(parser, ann)
+}
+
+func setAnnotationToParser(parser *rdfParser2_2, annotation *v2_2.Annotation) error {
+ if parser.doc == nil {
+ return errors.New("uninitialized spdx document")
+ }
+ if parser.doc.Annotations == nil {
+ parser.doc.Annotations = []*v2_2.Annotation{}
+ }
+ parser.doc.Annotations = append(parser.doc.Annotations, annotation)
+ return nil
+}
+
+// annotator is of type [Person|Organization|Tool]:String
+func setAnnotatorFromString(annotatorString string, ann *v2_2.Annotation) error {
+ subkey, subvalue, err := ExtractSubs(annotatorString, ":")
+ if err != nil {
+ return err
+ }
+ if subkey == "Person" || subkey == "Organization" || subkey == "Tool" {
+ ann.Annotator.AnnotatorType = subkey
+ ann.Annotator.Annotator = subvalue
+ return nil
+ }
+ return fmt.Errorf("unrecognized Annotator type %v while parsing annotation", subkey)
+}
+
+// it can be NS_SPDX+annotationType_[review|other]
+func setAnnotationType(annType string, ann *v2_2.Annotation) error {
+ switch annType {
+ case SPDX_ANNOTATION_TYPE_OTHER:
+ ann.AnnotationType = "OTHER"
+ case SPDX_ANNOTATION_TYPE_REVIEW:
+ ann.AnnotationType = "REVIEW"
+ default:
+ return fmt.Errorf("unknown annotation type %s", annType)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v2/parse_annotation_test.go b/rdfloader/parser2v2/parse_annotation_test.go
new file mode 100644
index 0000000..114fc5d
--- /dev/null
+++ b/rdfloader/parser2v2/parse_annotation_test.go
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func Test_setAnnotatorFromString(t *testing.T) {
+ // TestCase 1: Empty String must raise an error
+ ann := &v2_2.Annotation{}
+ input := ""
+ err := setAnnotatorFromString(input, ann)
+ if err == nil {
+ t.Error("should've raised an error for an empty string")
+ }
+
+ // TestCase 2: Invalid annotator type
+ ann = &v2_2.Annotation{}
+ input = "Company: some_company"
+ err = setAnnotatorFromString(input, ann)
+ if err == nil {
+ t.Errorf("should've raised an error for an unknown annotator type")
+ }
+
+ // TestCase 3: Valid annotator
+ ann = &v2_2.Annotation{}
+ input = "Person: Rishabh"
+ err = setAnnotatorFromString(input, ann)
+ if err != nil {
+ t.Errorf("unexpected error for a valid annotator")
+ }
+ if ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf("wrnog annotator type: expected: %s, found: %s", "Person", ann.Annotator)
+ }
+ if ann.Annotator.Annotator != "Rishabh" {
+ t.Errorf("wrong annotator: expected: %s, found: %s", "Rishabh", ann.Annotator)
+ }
+}
+
+func Test_setAnnotationType(t *testing.T) {
+ ann := &v2_2.Annotation{}
+ // TestCase 1: invalid input (empty annotationType)
+ err := setAnnotationType("", ann)
+ if err == nil {
+ t.Errorf("expected an error for empty input")
+ }
+
+ // TestCase 2: invalid input (unknown annotation type)
+ err = setAnnotationType(NS_SPDX+"annotationType_unknown", ann)
+ if err == nil {
+ t.Errorf("expected an error for invalid annotationType")
+ }
+
+ // TestCase 3: valid input (annotationType_other)
+ err = setAnnotationType(SPDX_ANNOTATION_TYPE_OTHER, ann)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ann.AnnotationType != "OTHER" {
+ t.Errorf("expected: OTHER, found: %s", ann.AnnotationType)
+ }
+
+ // TestCase 4: valid input (annotationType_review)
+ err = setAnnotationType(SPDX_ANNOTATION_TYPE_REVIEW, ann)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ann.AnnotationType != "REVIEW" {
+ t.Errorf("expected: REVIEW, found: %s", ann.AnnotationType)
+ }
+}
+
+func Test_setAnnotationToParser(t *testing.T) {
+ // TestCase 1: doc is nil (must raise an error)
+ parser, _ := parserFromBodyContent(``)
+ parser.doc = nil
+ err := setAnnotationToParser(parser, &v2_2.Annotation{})
+ if err == nil {
+ t.Errorf("empty doc should've raised an error")
+ }
+
+ // TestCase 2: empty annotations should create a new annotations
+ // list and append the input to it.
+ parser, _ = parserFromBodyContent(``)
+ parser.doc.Annotations = nil
+ err = setAnnotationToParser(parser, &v2_2.Annotation{})
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if len(parser.doc.Annotations) != 1 {
+ t.Errorf("expected doc to have 1 annotation, found %d", len(parser.doc.Annotations))
+ }
+}
+
+func Test_rdfParser2_2_parseAnnotationFromNode(t *testing.T) {
+ // TestCase 1: invalid annotator must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Company: some company</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ err := parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("wrong annotator type should've raised an error")
+ }
+
+ // TestCase 2: wrong annotation type should raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_unknown"/>
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("wrong annotation type should've raised an error")
+ }
+
+ // TestCase 3: unknown predicate should also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_unknown"/>
+ <spdx:unknownPredicate />
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("unknown predicate must raise an error")
+ }
+
+ // TestCase 4: completely valid annotation
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing valid a annotation")
+ }
+ if n := len(parser.doc.Annotations); n != 1 {
+ t.Errorf("document should've had only one annotation, found %d", n)
+ }
+ ann := parser.doc.Annotations[0]
+ // validating all the attributes of the annotations
+ expectedComment := "Document level annotation"
+ if ann.AnnotationComment != expectedComment {
+ t.Errorf(`expected: "%s", found "%s"`, expectedComment, ann.AnnotationComment)
+ }
+ expectedDate := "2010-01-29T18:30:22Z"
+ if expectedDate != ann.AnnotationDate {
+ t.Errorf(`expected: "%s", found "%s"`, expectedDate, ann.AnnotationDate)
+ }
+ expectedAnnotator := "Jane Doe"
+ if expectedAnnotator != ann.Annotator.Annotator {
+ t.Errorf(`expected: "%s", found "%s"`, expectedAnnotator, ann.Annotator)
+ }
+ if ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf(`expected: "%s", found "%s"`, "Person", ann.Annotator.AnnotatorType)
+ }
+ expectedAnnotationType := "OTHER"
+ if expectedAnnotationType != ann.AnnotationType {
+ t.Errorf(`expected: "%s", found "%s"`, expectedAnnotationType, ann.AnnotationType)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_creation_info.go b/rdfloader/parser2v2/parse_creation_info.go
new file mode 100644
index 0000000..b58149d
--- /dev/null
+++ b/rdfloader/parser2v2/parse_creation_info.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// Cardinality: Mandatory, one.
+func (parser *rdfParser2_2) parseCreationInfoFromNode(ci *v2_2.CreationInfo, node *gordfParser.Node) error {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_LICENSE_LIST_VERSION: // 2.7
+ // cardinality: max 1
+ ci.LicenseListVersion = triple.Object.ID
+ case SPDX_CREATOR: // 2.8
+ // cardinality: min 1
+ err := setCreator(triple.Object.ID, ci)
+ if err != nil {
+ return err
+ }
+ case SPDX_CREATED: // 2.9
+ // cardinality: exactly 1
+ ci.Created = triple.Object.ID
+ case RDFS_COMMENT: // 2.10
+ ci.CreatorComment = triple.Object.ID
+ case RDF_TYPE:
+ continue
+ default:
+ return fmt.Errorf("unknown predicate %v while parsing a creation info", triple.Predicate)
+ }
+ }
+ return nil
+}
+
+func setCreator(creatorStr string, ci *v2_2.CreationInfo) error {
+ entityType, entity, err := ExtractSubs(creatorStr, ":")
+ if err != nil {
+ return fmt.Errorf("error setting creator of a creation info: %s", err)
+ }
+
+ creator := common.Creator{Creator: entity}
+
+ switch entityType {
+ case "Person", "Organization", "Tool":
+ creator.CreatorType = entityType
+ default:
+ return fmt.Errorf("unknown creatorType %v in a creation info", entityType)
+ }
+
+ ci.Creators = append(ci.Creators, creator)
+
+ return nil
+}
diff --git a/rdfloader/parser2v2/parse_creation_info_test.go b/rdfloader/parser2v2/parse_creation_info_test.go
new file mode 100644
index 0000000..81fceae
--- /dev/null
+++ b/rdfloader/parser2v2/parse_creation_info_test.go
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func Test_setCreator(t *testing.T) {
+ // TestCase 1: invalid creator (empty)
+ input := ""
+ err := setCreator(input, &v2_2.CreationInfo{})
+ if err == nil {
+ t.Errorf("shoud've raised an error due to invalid input")
+ }
+
+ // TestCase 2: invalid entity type
+ input = "Company: some company"
+ err = setCreator(input, &v2_2.CreationInfo{})
+ if err == nil {
+ t.Errorf("shoud've raised an error due to unknown entity type")
+ }
+
+ // TestCase 3: valid input
+ input = "Person: Jane Doe"
+ ci := &v2_2.CreationInfo{}
+ err = setCreator(input, ci)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ if len(ci.Creators) != 1 {
+ t.Errorf("creationInfo should've had 1 creatorPersons, found %d", len(ci.Creators))
+ }
+ expectedPerson := "Jane Doe"
+ if ci.Creators[0].Creator != expectedPerson {
+ t.Errorf("expected %s, found %s", expectedPerson, ci.Creators[0])
+ }
+}
+
+func Test_rdfParser2_2_parseCreationInfoFromNode(t *testing.T) {
+ // TestCase 1: invalid creator must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person Unknown</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ </spdx:CreationInfo>
+ `)
+ ciNode := parser.gordfParserObj.Triples[0].Subject
+ err := parser.parseCreationInfoFromNode(&v2_2.CreationInfo{}, ciNode)
+ if err == nil {
+ t.Errorf("invalid creator must raise an error")
+ }
+
+ // TestCase 2: unknown predicate must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: fossy (y)</spdx:creator>
+ <spdx:creator>Organization: </spdx:creator>
+ <spdx:creator>Tool: spdx2</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ <spdx:unknownPredicate />
+ </spdx:CreationInfo>
+ `)
+ ciNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseCreationInfoFromNode(&v2_2.CreationInfo{}, ciNode)
+ if err == nil {
+ t.Errorf("unknown predicate must raise an error")
+ }
+
+ // TestCase 2: unknown predicate must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: fossy</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:CreationInfo>
+ `)
+ ciNode = parser.gordfParserObj.Triples[0].Subject
+ ci := &v2_2.CreationInfo{}
+ err = parser.parseCreationInfoFromNode(ci, ciNode)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ci.LicenseListVersion != "2.6" {
+ t.Errorf(`expected %s, found %s`, "2.6", ci.LicenseListVersion)
+ }
+ n := len(ci.Creators)
+ if n != 1 {
+ t.Errorf("expected 1 creatorPersons, found %d", n)
+ }
+ if ci.Creators[0].Creator != "fossy" {
+ t.Errorf("expected %s, found %s", "fossy", ci.Creators[0].Creator)
+ }
+ expectedCreated := "2018-08-24T19:55:34Z"
+ if ci.Created != expectedCreated {
+ t.Errorf("expected %s, found %s", expectedCreated, ci.Created)
+ }
+ expectedComment := "comment"
+ if ci.CreatorComment != expectedComment {
+ t.Errorf("expected %s, found %s", expectedComment, ci.CreatorComment)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_file.go b/rdfloader/parser2v2/parse_file.go
new file mode 100644
index 0000000..d5a19ef
--- /dev/null
+++ b/rdfloader/parser2v2/parse_file.go
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// returns a file instance and the error if any encountered.
+func (parser *rdfParser2_2) getFileFromNode(fileNode *gordfParser.Node) (file *v2_2.File, err error) {
+ file = &v2_2.File{}
+
+ currState := parser.cache[fileNode.ID]
+ if currState == nil {
+ // this is the first time we are seeing this node.
+ parser.cache[fileNode.ID] = &nodeState{
+ object: file,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this file node and we needn't parse it again.
+ return currState.object.(*v2_2.File), nil
+ }
+
+ // setting color to grey to indicate that we've started parsing this node.
+ parser.cache[fileNode.ID].Color = GREY
+
+ // setting color to black just before function returns to the caller to
+ // indicate that parsing current node is complete.
+ defer func() { parser.cache[fileNode.ID].Color = BLACK }()
+
+ err = setFileIdentifier(fileNode.ID, file) // 4.2
+ if err != nil {
+ return nil, err
+ }
+
+ if existingFile := parser.files[file.FileSPDXIdentifier]; existingFile != nil {
+ file = existingFile
+ }
+
+ for _, subTriple := range parser.nodeToTriples(fileNode) {
+ switch subTriple.Predicate.ID {
+ case SPDX_FILE_NAME: // 4.1
+ // cardinality: exactly 1
+ file.FileName = subTriple.Object.ID
+ case SPDX_NAME:
+ // cardinality: exactly 1
+ // TODO: check where it will be set in the golang-tools spdx-data-model
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ case SPDX_FILE_TYPE: // 4.3
+ // cardinality: min 0
+ fileType := ""
+ fileType, err = parser.getFileTypeFromUri(subTriple.Object.ID)
+ file.FileTypes = append(file.FileTypes, fileType)
+ case SPDX_CHECKSUM: // 4.4
+ // cardinality: min 1
+ err = parser.setFileChecksumFromNode(file, subTriple.Object)
+ case SPDX_LICENSE_CONCLUDED: // 4.5
+ // cardinality: (exactly 1 anyLicenseInfo) or (None) or (Noassertion)
+ anyLicense, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing licenseConcluded: %v", err)
+ }
+ file.LicenseConcluded = anyLicense.ToLicenseString()
+ case SPDX_LICENSE_INFO_IN_FILE: // 4.6
+ // cardinality: min 1
+ lic, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing licenseInfoInFile: %v", err)
+ }
+ file.LicenseInfoInFiles = append(file.LicenseInfoInFiles, lic.ToLicenseString())
+ case SPDX_LICENSE_COMMENTS: // 4.7
+ // cardinality: max 1
+ file.LicenseComments = subTriple.Object.ID
+ // TODO: allow copyright text to be of type NOASSERTION
+ case SPDX_COPYRIGHT_TEXT: // 4.8
+ // cardinality: exactly 1
+ file.FileCopyrightText = subTriple.Object.ID
+ case SPDX_LICENSE_INFO_FROM_FILES:
+ // TODO: implement it. It is not defined in the tools-golang model.
+ // deprecated artifactOf (see sections 4.9, 4.10, 4.11)
+ case SPDX_ARTIFACT_OF:
+ // cardinality: min 0
+ var artifactOf *v2_2.ArtifactOfProject
+ artifactOf, err = parser.getArtifactFromNode(subTriple.Object)
+ file.ArtifactOfProjects = append(file.ArtifactOfProjects, artifactOf)
+ case RDFS_COMMENT: // 4.12
+ // cardinality: max 1
+ file.FileComment = subTriple.Object.ID
+ case SPDX_NOTICE_TEXT: // 4.13
+ // cardinality: max 1
+ file.FileNotice = getNoticeTextFromNode(subTriple.Object)
+ case SPDX_FILE_CONTRIBUTOR: // 4.14
+ // cardinality: min 0
+ file.FileContributors = append(file.FileContributors, subTriple.Object.ID)
+ case SPDX_FILE_DEPENDENCY:
+ // cardinality: min 0
+ newFile, err := parser.getFileFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error setting a file dependency in a file: %v", err)
+ }
+ file.FileDependencies = append(file.FileDependencies, string(newFile.FileSPDXIdentifier))
+ case SPDX_ATTRIBUTION_TEXT:
+ // cardinality: min 0
+ file.FileAttributionTexts = append(file.FileAttributionTexts, subTriple.Object.ID)
+ case SPDX_ANNOTATION:
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ case SPDX_RELATIONSHIP:
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ default:
+ return nil, fmt.Errorf("unknown triple predicate id %s", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+ parser.files[file.FileSPDXIdentifier] = file
+ return file, nil
+}
+
+func (parser *rdfParser2_2) setFileChecksumFromNode(file *v2_2.File, checksumNode *gordfParser.Node) error {
+ checksumAlgorithm, checksumValue, err := parser.getChecksumFromNode(checksumNode)
+ if err != nil {
+ return fmt.Errorf("error parsing checksumNode of a file: %v", err)
+ }
+ if file.Checksums == nil {
+ file.Checksums = []common.Checksum{}
+ }
+ switch checksumAlgorithm {
+ case common.SHA1,
+ common.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6:
+ file.Checksums = append(file.Checksums, common.Checksum{Algorithm: checksumAlgorithm, Value: checksumValue})
+ case "":
+ return fmt.Errorf("empty checksum algorithm and value")
+ default:
+ return fmt.Errorf("unknown checksumAlgorithm %s for a file", checksumAlgorithm)
+ }
+ return nil
+}
+
+func (parser *rdfParser2_2) getArtifactFromNode(node *gordfParser.Node) (*v2_2.ArtifactOfProject, error) {
+ artifactOf := &v2_2.ArtifactOfProject{}
+ // setting artifactOfProjectURI attribute (which is optional)
+ if node.NodeType == gordfParser.IRI {
+ artifactOf.URI = node.ID
+ }
+ // parsing rest triples and attributes of the artifact.
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ case DOAP_HOMEPAGE:
+ artifactOf.HomePage = triple.Object.ID
+ case DOAP_NAME:
+ artifactOf.Name = triple.Object.ID
+ default:
+ return nil, fmt.Errorf("error parsing artifactOf predicate %s", triple.Predicate.ID)
+ }
+ }
+ return artifactOf, nil
+}
+
+// TODO: check if the filetype is valid.
+func (parser *rdfParser2_2) getFileTypeFromUri(uri string) (string, error) {
+ // fileType is given as a uri. for example: http://spdx.org/rdf/terms#fileType_text
+ lastPart := getLastPartOfURI(uri)
+ if !strings.HasPrefix(lastPart, "fileType_") {
+ return "", fmt.Errorf("fileType Uri must begin with fileTYpe_. found: %s", lastPart)
+ }
+ return strings.TrimPrefix(lastPart, "fileType_"), nil
+}
+
+// populates parser.doc.Files by a list of files which are not
+// associated with a package by the hasFile attribute
+// assumes: all the packages are already parsed.
+func (parser *rdfParser2_2) setUnpackagedFiles() {
+ for fileID := range parser.files {
+ if !parser.assocWithPackage[fileID] {
+ parser.doc.Files = append(parser.doc.Files, parser.files[fileID])
+ }
+ }
+}
+
+func setFileIdentifier(idURI string, file *v2_2.File) (err error) {
+ idURI = strings.TrimSpace(idURI)
+ uriFragment := getLastPartOfURI(idURI)
+ file.FileSPDXIdentifier, err = ExtractElementID(uriFragment)
+ if err != nil {
+ return fmt.Errorf("error setting file identifier: %s", err)
+ }
+ return nil
+}
+
+func getNoticeTextFromNode(node *gordfParser.Node) string {
+ switch node.ID {
+ case SPDX_NOASSERTION_CAPS, SPDX_NOASSERTION_SMALL:
+ return "NOASSERTION"
+ default:
+ return node.ID
+ }
+}
diff --git a/rdfloader/parser2v2/parse_file_test.go b/rdfloader/parser2v2/parse_file_test.go
new file mode 100644
index 0000000..0383cd9
--- /dev/null
+++ b/rdfloader/parser2v2/parse_file_test.go
@@ -0,0 +1,765 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "bufio"
+ "strings"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ rdfloader2 "github.com/spdx/gordf/rdfloader/xmlreader"
+ gordfWriter "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// content is the tags within the rdf:RDF tag
+// pads the content with the enclosing rdf:RDF tag
+func wrapIntoTemplate(content string) string {
+ header := `<rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#"
+ xmlns:doap="http://usefulinc.com/ns/doap#"
+ xmlns:j.0="http://www.w3.org/2009/pointers#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">`
+ footer := `</rdf:RDF>`
+ return header + content + footer
+}
+
+func parserFromBodyContent(content string) (*rdfParser2_2, error) {
+ rdfContent := wrapIntoTemplate(content)
+ xmlreader := rdfloader2.XMLReaderFromFileObject(bufio.NewReader(strings.NewReader(rdfContent)))
+ rootBlock, err := xmlreader.Read()
+ if err != nil {
+ return nil, err
+ }
+ parser := gordfParser.New()
+ err = parser.Parse(rootBlock)
+ if err != nil {
+ return nil, err
+ }
+ nodeToTriples := gordfWriter.GetNodeToTriples(parser.Triples)
+ rdfParser := NewParser2_2(parser, nodeToTriples)
+ return rdfParser, err
+}
+
+func Test_rdfParser2_2_getArtifactFromNode(t *testing.T) {
+ // TestCase 1: artifactOf without project URI
+ rdfParser, err := parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode := gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ artifact, err := rdfParser.getArtifactFromNode(artifactOfNode)
+ if err != nil {
+ t.Errorf("error parsing a valid artifactOf node: %v", err)
+ }
+ if artifact.Name != "Jena" {
+ t.Errorf("expected name of artifact: %s, found: %s", "Jena", artifact.Name)
+ }
+ expectedHomePage := "http://www.openjena.org/"
+ if artifact.HomePage != expectedHomePage {
+ t.Errorf("wrong artifact homepage. Expected: %s, found: %s", expectedHomePage, artifact.HomePage)
+ }
+ if artifact.URI != "" {
+ t.Errorf("wrong artifact URI. Expected: %s, found: %s", "", artifact.URI)
+ }
+
+ // TestCase 2: artifactOf with a Project URI
+ rdfParser, err = parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode = gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ artifact, err = rdfParser.getArtifactFromNode(artifactOfNode)
+ if err != nil {
+ t.Errorf("error parsing a valid artifactOf node: %v", err)
+ }
+ expectedURI := "http://subversion.apache.org/doap.rdf"
+ if artifact.URI != expectedURI {
+ t.Errorf("wrong artifact URI. Expected: %s, found: %s", expectedURI, artifact.URI)
+ }
+
+ // TestCase 3: artifactOf with unknown predicate
+ rdfParser, err = parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ <doap:invalidTag rdf:ID="invalid"/>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode = gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ _, err = rdfParser.getArtifactFromNode(artifactOfNode)
+ if err == nil {
+ t.Errorf("must've raised an error for an invalid predicate")
+ }
+}
+
+func Test_rdfParser2_2_getFileTypeFromUri(t *testing.T) {
+ rdfParser, _ := parserFromBodyContent(``)
+
+ // TestCase 1: Valid fileType URI:
+ fileTypeURI := "http://spdx.org/rdf/terms#fileType_source"
+ fileType, err := rdfParser.getFileTypeFromUri(fileTypeURI)
+ if err != nil {
+ t.Errorf("error in a valid example: %v", err)
+ }
+ if fileType != "source" {
+ t.Errorf("wrong fileType. expected: %s, found: %s", "source", fileType)
+ }
+
+ // TestCase 2: Invalid fileType URI format.
+ fileTypeURI = "http://spdx.org/rdf/terms#source"
+ fileType, err = rdfParser.getFileTypeFromUri(fileTypeURI)
+ if err == nil {
+ t.Error("should've raised an error for invalid fileType")
+ }
+}
+
+func Test_rdfParser2_2_setUnpackagedFiles(t *testing.T) {
+ // unpackaged files are the files which are not associated with any package
+ // file associated with a package sets parser.assocWithPackage[fileID] to true.
+ rdfParser, _ := parserFromBodyContent(``)
+ file1 := &v2_2.File{FileSPDXIdentifier: common.ElementID("file1")}
+ file2 := &v2_2.File{FileSPDXIdentifier: common.ElementID("file2")}
+ file3 := &v2_2.File{FileSPDXIdentifier: common.ElementID("file3")}
+
+ // setting files to the document as if it were to be set when it was parsed using triples.
+ rdfParser.files[file1.FileSPDXIdentifier] = file1
+ rdfParser.files[file2.FileSPDXIdentifier] = file2
+ rdfParser.files[file3.FileSPDXIdentifier] = file3
+
+ // assuming file1 is associated with a package
+ rdfParser.assocWithPackage[file1.FileSPDXIdentifier] = true
+
+ rdfParser.setUnpackagedFiles()
+
+ // after setting unpackaged files, parser.doc.Files must've file2 and file3
+ if n := len(rdfParser.doc.Files); n != 2 {
+ t.Errorf("unpackage files should've had 2 files, found %d files", n)
+ }
+
+ // checking if the unpackagedFiles contain only file2 & file3.
+ for _, file := range rdfParser.doc.Files {
+ switch string(file.FileSPDXIdentifier) {
+ case "file2", "file3":
+ continue
+ default:
+ t.Errorf("unexpected file with id %s found in unpackaged files", file.FileSPDXIdentifier)
+ }
+ }
+}
+
+func Test_setFileIdentifier(t *testing.T) {
+ file := &v2_2.File{}
+
+ // TestCase 1: valid example
+ err := setFileIdentifier("http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-129", file)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if file.FileSPDXIdentifier != "129" {
+ t.Errorf("expected %s, found: %s", "129", file.FileSPDXIdentifier)
+ }
+
+ // TestCase 2: invalid example
+ err = setFileIdentifier("http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#129", file)
+ if err == nil {
+ t.Errorf("should've raised an error for an invalid example")
+ }
+}
+
+func Test_rdfParser2_2_setFileChecksumFromNode(t *testing.T) {
+ // TestCase 1: md5 checksum
+ parser, _ := parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file := &v2_2.File{}
+ err := parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ checksumValue := "d2356e0fe1c0b85285d83c6b2ad51b5f"
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha1, should've been empty")
+ }
+ case common.SHA256:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha256, should've been empty")
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for md5. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 2: valid sha1 checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_2.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for sha1. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha256, should've been empty")
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set md5, should've been empty")
+ }
+ }
+ }
+
+ // TestCase 3: valid sha256 checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_2.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set sha1, should've been empty")
+ }
+ case common.SHA256:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for sha256. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set md5, should've been empty")
+ }
+ }
+ }
+
+ // TestCase 4: checksum node without one of the mandatory attributes
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_2.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum node")
+ }
+
+ // TestCase 5: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md43" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_2.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum node")
+ }
+
+ // TestCase 6: valid checksum algorithm which is invalid for file (like md4, md6, sha384, etc.)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha2000" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_2.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum algorithm for a file")
+ }
+}
+
+func Test_rdfParser2_2_getFileFromNode(t *testing.T) {
+ // TestCase 1: file with invalid id
+ parser, _ := parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gzspdx.rdf#item177"/>
+ `)
+ fileNode := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err := parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid file ID")
+ }
+
+ // TestCase 2: invalid fileType
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#source"/>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid fileType")
+ }
+
+ // TestCase 3: invalid file checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha2000" />
+ <spdx:checksumValue>0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid checksum")
+ }
+
+ // TestCase 4: invalid license concluded
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#invalid_license" />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid license Concluded")
+ }
+
+ // TestCase 5: invalid artifactOf attribute
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:unknown_tag />
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid artifactOf predicate")
+ }
+
+ // TestCase 6: invalid file dependency
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example#CommonsLangSrc"/>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid fileDependency")
+ }
+
+ // TestCase 7: invalid annotation with unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid annotation predicate")
+ }
+
+ // TestCase 8: invalid relationship
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#dynamicLink"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid relationship Type")
+ }
+
+ // TestCase 8: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:unknown />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Error("should've raised an error stating invalid predicate for a file")
+ }
+
+ // TestCase 9: invalid licenseInfoInFile.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/DC0-1.0" />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Error("should've raised an error stating invalid licenseInfoInFile for a file")
+ }
+
+ // TestCase 10: Splitting of File definition into parents of different tags mustn't create new file objects.
+ fileDefinitions := []string{
+ `<spdx:Package rdf:about="#SPDXRef-Package1">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileName>time-1.9/ChangeLog</spdx:fileName>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ </spdx:File>
+ </spdx:hasFile>
+ </spdx:Package>`,
+ `<spdx:Package rdf:about="#SPDXRef-Package2">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/rdf/terms#NOASSERTION" />
+ </spdx:File>
+ </spdx:hasFile>
+ </spdx:Package>`,
+ }
+ parser, _ = parserFromBodyContent(strings.Join(fileDefinitions, ""))
+
+ var file *v2_2.File
+ packageTypeTriples := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)
+ for _, typeTriple := range packageTypeTriples {
+ pkg, err := parser.getPackageFromNode(typeTriple.Subject)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid package: %v", err)
+ }
+ if n := len(pkg.Files); n != 1 {
+ t.Errorf("expected package to contain exactly 1 file. Found %d files", n)
+ }
+ for _, file = range pkg.Files {
+ }
+ }
+
+ // checking if all the attributes that spanned over a several tags are set in the same variable.
+ expectedFileName := "time-1.9/ChangeLog"
+ if file.FileName != expectedFileName {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileName)
+ }
+ expectedLicenseConcluded := "NOASSERTION"
+ if file.LicenseConcluded != expectedLicenseConcluded {
+ t.Errorf("expected %s, found %s", expectedLicenseConcluded, file.LicenseConcluded)
+ }
+ expectedFileType := "source"
+ if file.FileTypes[0] != expectedFileType {
+ t.Errorf("expected %s, found %s", expectedFileType, file.FileTypes)
+ }
+ expectedLicenseInfoInFile := "NOASSERTION"
+ if file.LicenseInfoInFiles[0] != expectedLicenseInfoInFile {
+ t.Errorf("expected %s, found %s", expectedLicenseInfoInFile, file.LicenseInfoInFiles[0])
+ }
+
+ // TestCase 12: checking if recursive dependencies are resolved.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="#SPDXRef-ParentFile">
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:fileDependency>
+ <spdx:File rdf:about="#SPDXRef-ChildFile">
+ <spdx:fileDependency>
+ <spdx:File rdf:about="#SPDXRef-ParentFile">
+ <spdx:fileName>ParentFile</spdx:fileName>
+ </spdx:File>
+ </spdx:fileDependency>
+ </spdx:File>
+ </spdx:fileDependency>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ file, err = parser.getFileFromNode(fileNode)
+
+ // TestCase 11: all valid attribute and it's values.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileName>time-1.9/ChangeLog</spdx:fileName>
+ <spdx:name/>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/rdf/terms#NOASSERTION" />
+ <spdx:licenseComments>no comments</spdx:licenseComments>
+ <spdx:copyrightText>from spdx file</spdx:copyrightText>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:noticeText rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ <spdx:fileContributor>Some Organization</spdx:fileContributor>
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-CommonsLangSrc"/>
+ <spdx:attributionText>attribution text</spdx:attributionText>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>File level annotation copied from a spdx document</rdfs:comment>
+ <spdx:annotator>Person: File Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_contains"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Package"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ file, err = parser.getFileFromNode(fileNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid file: %v", err)
+ }
+
+ // checking each and every attribute of the obtained file.
+
+ expectedFileName = "time-1.9/ChangeLog"
+ if file.FileName != expectedFileName {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileName)
+ }
+
+ if len(file.FileTypes) != 1 {
+ t.Errorf("given file should have 1 fileType attribute. found %d", len(file.FileTypes))
+ }
+ expectedFileType = "source"
+ if file.FileTypes[0] != expectedFileType {
+ t.Errorf("expected %s, found %s", expectedFileType, file.FileTypes)
+ }
+
+ expectedChecksum := "0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd"
+
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != expectedChecksum {
+ t.Errorf("expected %s, found %s", expectedChecksum, checksum.Value)
+ }
+ }
+ }
+
+ expectedLicenseConcluded = "NOASSERTION"
+ if file.LicenseConcluded != expectedLicenseConcluded {
+ t.Errorf("expected %s, found %s", expectedLicenseConcluded, file.LicenseConcluded)
+ }
+
+ if len(file.LicenseInfoInFiles) != 1 {
+ t.Errorf("given file should have 1 licenseInfoInFile attribute. found %d", len(file.LicenseInfoInFiles))
+ }
+ expectedLicenseInfoInFile = "NOASSERTION"
+ if file.LicenseInfoInFiles[0] != expectedLicenseInfoInFile {
+ t.Errorf("expected %s, found %s", expectedLicenseInfoInFile, file.LicenseInfoInFiles[0])
+ }
+
+ expectedLicenseComments := "no comments"
+ if file.LicenseComments != expectedLicenseComments {
+ t.Errorf("expected %s, found %s", expectedLicenseComments, file.LicenseComments)
+ }
+
+ expectedCopyrightText := "from spdx file"
+ if file.FileCopyrightText != expectedCopyrightText {
+ t.Errorf("expected %s, found %s", expectedCopyrightText, file.FileCopyrightText)
+ }
+
+ if n := len(file.ArtifactOfProjects); n != 1 {
+ t.Errorf("given file should have 1 artifactOfProjects attribute. found %d", n)
+ }
+ artifactOf := file.ArtifactOfProjects[0]
+ expectedHomePage := "http://www.openjena.org/"
+ if artifactOf.HomePage != expectedHomePage {
+ t.Errorf("expected %s, found %s", expectedHomePage, artifactOf.HomePage)
+ }
+ if artifactOf.Name != "Jena" {
+ t.Errorf("expected %s, found %s", "Jena", artifactOf.Name)
+ }
+ if artifactOf.URI != "" {
+ t.Errorf("expected artifactOf uri to be empty, found %s", artifactOf.URI)
+ }
+
+ expectedFileComment := "no comments"
+ if file.FileComment != expectedFileComment {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileComment)
+ }
+
+ expectedNoticeText := "NOASSERTION"
+ if file.FileNotice != expectedNoticeText {
+ t.Errorf("expected %s, found %s", expectedNoticeText, file.FileNotice)
+ }
+
+ if n := len(file.FileContributors); n != 1 {
+ t.Errorf("given file should have 1 fileContributor. found %d", n)
+ }
+ expectedFileContributor := "Some Organization"
+ if file.FileContributors[0] != expectedFileContributor {
+ t.Errorf("expected %s, found %s", expectedFileContributor, file.FileContributors)
+ }
+
+ if n := len(file.FileDependencies); n != 1 {
+ t.Errorf("given file should have 1 fileDependencies. found %d", n)
+ }
+ expectedFileDependency := "CommonsLangSrc"
+ if file.FileDependencies[0] != expectedFileDependency {
+ t.Errorf("expected %s, found %s", expectedFileDependency, file.FileDependencies[0])
+ }
+
+ if n := len(file.FileAttributionTexts); n != 1 {
+ t.Errorf("given file should have 1 attributionText. found %d", n)
+ }
+ expectedAttributionText := "attribution text"
+ if file.FileAttributionTexts[0] != expectedAttributionText {
+ t.Errorf("expected %s, found %s", expectedAttributionText, file.FileAttributionTexts[0])
+ }
+
+ if n := len(parser.doc.Annotations); n != 1 {
+ t.Errorf("doc should've had 1 annotation. found %d", n)
+ }
+ ann := parser.doc.Annotations[0]
+ expectedAnnDate := "2011-01-29T18:30:22Z"
+ if ann.AnnotationDate != expectedAnnDate {
+ t.Errorf("expected %s, found %s", expectedAnnDate, ann.AnnotationDate)
+ }
+ expectedAnnComment := "File level annotation copied from a spdx document"
+ if ann.AnnotationComment != expectedAnnComment {
+ t.Errorf("expected %s, found %s", expectedAnnComment, ann.AnnotationComment)
+ }
+ expectedAnnotationType := "OTHER"
+ if ann.AnnotationType != expectedAnnotationType {
+ t.Errorf("expected %s, found %s", expectedAnnotationType, ann.AnnotationType)
+ }
+ expectedAnnotator := "File Commenter"
+ if ann.Annotator.Annotator != expectedAnnotator {
+ t.Errorf("expected %s, found %s", expectedAnnotator, ann.Annotator)
+ }
+ expectedAnnotatorType := "Person"
+ if ann.AnnotationType != expectedAnnotationType {
+ t.Errorf("expected %s, found %s", expectedAnnotatorType, ann.Annotator.AnnotatorType)
+ }
+
+ if n := len(parser.doc.Relationships); n != 1 {
+ t.Errorf("doc should've had 1 relation. found %d", n)
+ }
+ reln := parser.doc.Relationships[0]
+ expectedRefAEID := "item177"
+ if reln.RefA.DocumentRefID != "" {
+ t.Errorf("expected refA.DocumentRefID to be empty, found %s", reln.RefA.DocumentRefID)
+ }
+ if string(reln.RefA.ElementRefID) != expectedRefAEID {
+ t.Errorf("expected %s, found %s", expectedRefAEID, reln.RefA.ElementRefID)
+ }
+ expectedRefBEID := "Package"
+ if reln.RefB.DocumentRefID != "" {
+ t.Errorf("expected refB.DocumentRefID to be empty, found %s", reln.RefB.DocumentRefID)
+ }
+ if string(reln.RefB.ElementRefID) != expectedRefBEID {
+ t.Errorf("expected %s, found %s", expectedRefBEID, reln.RefB.ElementRefID)
+ }
+ expectedRelationType := "contains"
+ if reln.Relationship != expectedRelationType {
+ t.Errorf("expected %s, found %s", expectedRefBEID, reln.RefB.ElementRefID)
+ }
+ if reln.RelationshipComment != "" {
+ t.Errorf("expected relationship comment to be empty, found %s", reln.RelationshipComment)
+ }
+}
+
+func Test_getNoticeTextFromNode(t *testing.T) {
+ // TestCase 1: SPDX_NOASSERTION_SMALL must return NOASSERTION
+ output := getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_NOASSERTION_SMALL,
+ })
+ if strings.ToUpper(output) != "NOASSERTION" {
+ t.Errorf("expected NOASSERTION, found %s", strings.ToUpper(output))
+ }
+
+ // TestCase 2: SPDX_NOASSERTION_CAPS must return NOASSERTION
+ output = getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_NOASSERTION_CAPS,
+ })
+ if strings.ToUpper(output) != "NOASSERTION" {
+ t.Errorf("expected NOASSERTION, found %s", strings.ToUpper(output))
+ }
+
+ // TestCase 3: not a NOASSERTION must return the field verbatim
+ // TestCase 1: SPDX_NOASSERTION_SMALL must return NOASSERTION
+ output = getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "text",
+ })
+ if output != "text" {
+ t.Errorf("expected text, found %s", output)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_license.go b/rdfloader/parser2v2/parse_license.go
new file mode 100644
index 0000000..71a1e4a
--- /dev/null
+++ b/rdfloader/parser2v2/parse_license.go
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+)
+
+// AnyLicense is a baseClass for all the licenses
+// All the types of licenses is a sub-type of AnyLicense,
+// either directly or indirectly.
+// This function acts as a mux for all the licenses. Based on the input, it
+// decides which type of license it is and passes control to that type of
+// license parser to parse the given input.
+func (parser *rdfParser2_2) getAnyLicenseFromNode(node *gordfParser.Node) (AnyLicenseInfo, error) {
+
+ currState := parser.cache[node.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[node.ID] = &nodeState{
+ object: nil, // not storing the object as we won't retrieve it later.
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this license node.
+ // We have a cyclic dependency!
+ return nil, errors.New("Couldn't parse license: found a cyclic dependency on " + node.ID)
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[node.ID].Color = GREY
+
+ // setting state color to black when we're done parsing this node.
+ defer func() { parser.cache[node.ID].Color = BLACK }()
+
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ if len(associatedTriples) == 0 {
+ // just a license uri string was found.
+ return parser.getSpecialLicenseFromNode(node)
+ }
+
+ // we have some attributes associated with the license node.
+ nodeType, err := getNodeTypeFromTriples(associatedTriples, node)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license triple: %v", err)
+ }
+ switch nodeType {
+ case SPDX_DISJUNCTIVE_LICENSE_SET:
+ return parser.getDisjunctiveLicenseSetFromNode(node)
+ case SPDX_CONJUNCTIVE_LICENSE_SET:
+ return parser.getConjunctiveLicenseSetFromNode(node)
+ case SPDX_EXTRACTED_LICENSING_INFO:
+ return parser.getExtractedLicensingInfoFromNode(node)
+ case SPDX_LISTED_LICENSE, SPDX_LICENSE:
+ return parser.getLicenseFromNode(node)
+ case SPDX_WITH_EXCEPTION_OPERATOR:
+ return parser.getWithExceptionOperatorFromNode(node)
+ case SPDX_OR_LATER_OPERATOR:
+ return parser.getOrLaterOperatorFromNode(node)
+ case SPDX_SIMPLE_LICENSING_INFO:
+ return parser.getSimpleLicensingInfoFromNode(node)
+ }
+ return nil, fmt.Errorf("Unknown subTag (%s) found while parsing AnyLicense", nodeType)
+}
+
+func (parser *rdfParser2_2) getLicenseExceptionFromNode(node *gordfParser.Node) (exception LicenseException, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ for _, triple := range associatedTriples {
+ value := triple.Object.ID
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_LICENSE_EXCEPTION_ID:
+ exception.licenseExceptionId = value
+ case SPDX_LICENSE_EXCEPTION_TEXT:
+ exception.licenseExceptionText = value
+ case RDFS_SEE_ALSO:
+ if !isUriValid(value) {
+ return exception, fmt.Errorf("invalid uri (%s) for seeAlso attribute of LicenseException", value)
+ }
+ exception.seeAlso = value
+ case SPDX_NAME:
+ exception.name = value
+ case SPDX_EXAMPLE:
+ exception.example = value
+ case RDFS_COMMENT:
+ exception.comment = value
+ default:
+ return exception, fmt.Errorf("invalid predicate(%s) for LicenseException", triple.Predicate)
+ }
+ }
+ return exception, nil
+}
+
+func (parser *rdfParser2_2) getSimpleLicensingInfoFromNode(node *gordfParser.Node) (SimpleLicensingInfo, error) {
+ simpleLicensingTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ return parser.getSimpleLicensingInfoFromTriples(simpleLicensingTriples)
+}
+
+func (parser *rdfParser2_2) getWithExceptionOperatorFromNode(node *gordfParser.Node) (operator WithExceptionOperator, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var memberFound bool
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ if memberFound {
+ return operator,
+ fmt.Errorf("more than one member found in the WithExceptionOperator (expected only 1)")
+ }
+ memberFound = true
+ member, err := parser.getSimpleLicensingInfoFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing member of a WithExceptionOperator: %v", err)
+ }
+ operator.member = member
+ case SPDX_LICENSE_EXCEPTION:
+ operator.licenseException, err = parser.getLicenseExceptionFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing licenseException of WithExceptionOperator: %v", err)
+ }
+ default:
+ return operator, fmt.Errorf("unknown predicate (%s) for a WithExceptionOperator", triple.Predicate.ID)
+ }
+ }
+ return operator, nil
+}
+
+func (parser *rdfParser2_2) getOrLaterOperatorFromNode(node *gordfParser.Node) (operator OrLaterOperator, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ n := len(associatedTriples)
+ if n != 2 {
+ return operator, fmt.Errorf("orLaterOperator must be associated with exactly one tag. found %v triples", n-1)
+ }
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ operator.member, err = parser.getSimpleLicensingInfoFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing simpleLicensingInfo of OrLaterOperator: %v", err)
+ }
+ default:
+ return operator, fmt.Errorf("unknown predicate %s", triple.Predicate.ID)
+ }
+ }
+ return operator, nil
+}
+
+// SpecialLicense is a type of license which is not defined in any of the
+// spdx documents, it is a type of license defined for the sake of brevity.
+// It can be [NONE|NOASSERTION|LicenseRef-<string>]
+func (parser *rdfParser2_2) getSpecialLicenseFromNode(node *gordfParser.Node) (lic SpecialLicense, err error) {
+ uri := strings.TrimSpace(node.ID)
+ switch uri {
+ case SPDX_NONE_CAPS, SPDX_NONE_SMALL:
+ return SpecialLicense{
+ value: NONE,
+ }, nil
+ case SPDX_NOASSERTION_SMALL, SPDX_NOASSERTION_CAPS:
+ return SpecialLicense{
+ value: NOASSERTION,
+ }, nil
+ }
+
+ // the license is neither NONE nor NOASSERTION
+ // checking if the license is among the standardLicenses
+ licenseAbbreviation := getLastPartOfURI(uri)
+ for _, stdLicense := range AllStandardLicenseIDS() {
+ if licenseAbbreviation == stdLicense {
+ return SpecialLicense{
+ value: SpecialLicenseValue(stdLicense),
+ }, nil
+ }
+ }
+ return lic, fmt.Errorf("found a custom license uri (%s) without any associated fields", uri)
+}
+
+func (parser *rdfParser2_2) getDisjunctiveLicenseSetFromNode(node *gordfParser.Node) (DisjunctiveLicenseSet, error) {
+ licenseSet := DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{},
+ }
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ member, err := parser.getAnyLicenseFromNode(triple.Object)
+ if err != nil {
+ return licenseSet, fmt.Errorf("error parsing disjunctive license set: %v", err)
+ }
+ licenseSet.members = append(licenseSet.members, member)
+ }
+ }
+ return licenseSet, nil
+}
+
+func (parser *rdfParser2_2) getConjunctiveLicenseSetFromNode(node *gordfParser.Node) (ConjunctiveLicenseSet, error) {
+ licenseSet := ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{},
+ }
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ member, err := parser.getAnyLicenseFromNode(triple.Object)
+ if err != nil {
+ return licenseSet, fmt.Errorf("error parsing conjunctive license set: %v", err)
+ }
+ licenseSet.members = append(licenseSet.members, member)
+ default:
+ return licenseSet, fmt.Errorf("unknown subTag for ConjunctiveLicenseSet: %s", triple.Predicate.ID)
+ }
+ }
+ return licenseSet, nil
+}
+
+func (parser *rdfParser2_2) getSimpleLicensingInfoFromTriples(triples []*gordfParser.Triple) (lic SimpleLicensingInfo, err error) {
+ for _, triple := range triples {
+ switch triple.Predicate.ID {
+ case RDFS_COMMENT:
+ lic.comment = triple.Object.ID
+ case SPDX_LICENSE_ID:
+ lic.licenseID = triple.Object.ID
+ case SPDX_NAME:
+ lic.name = triple.Object.ID
+ case RDFS_SEE_ALSO:
+ if !isUriValid(triple.Object.ID) {
+ return lic, fmt.Errorf("%s is not a valid uri for seeAlso attribute of a License", triple.Object.ID)
+ }
+ lic.seeAlso = append(lic.seeAlso, triple.Object.ID)
+ case SPDX_EXAMPLE:
+ lic.example = triple.Object.ID
+ case RDF_TYPE:
+ continue
+ default:
+ return lic, fmt.Errorf("unknown predicate(%s) for simple licensing info", triple.Predicate)
+ }
+ }
+ return lic, nil
+}
+
+func (parser *rdfParser2_2) getLicenseFromNode(node *gordfParser.Node) (lic License, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var restTriples []*gordfParser.Triple
+ for _, triple := range associatedTriples {
+ value := triple.Object.ID
+ switch triple.Predicate.ID {
+ case SPDX_IS_OSI_APPROVED:
+ lic.isOsiApproved, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isOsiApproved attribute of a License: %v", err)
+ }
+ case SPDX_LICENSE_TEXT:
+ lic.licenseText = value
+ case SPDX_STANDARD_LICENSE_HEADER:
+ lic.standardLicenseHeader = value
+ case SPDX_STANDARD_LICENSE_TEMPLATE:
+ lic.standardLicenseTemplate = value
+ case SPDX_STANDARD_LICENSE_HEADER_TEMPLATE:
+ lic.standardLicenseHeaderTemplate = value
+ case SPDX_IS_DEPRECATED_LICENSE_ID:
+ lic.isDeprecatedLicenseID, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isDeprecatedLicenseId attribute of a License: %v", err)
+ }
+ case SPDX_IS_FSF_LIBRE:
+ lic.isFsfLibre, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isFsfLibre attribute of a License: %v", err)
+ }
+ default:
+ restTriples = append(restTriples, triple)
+ }
+ }
+ lic.SimpleLicensingInfo, err = parser.getSimpleLicensingInfoFromTriples(restTriples)
+ if err != nil {
+ return lic, fmt.Errorf("error setting simple licensing information of a License: %s", err)
+ }
+ return lic, nil
+}
diff --git a/rdfloader/parser2v2/parse_license_test.go b/rdfloader/parser2v2/parse_license_test.go
new file mode 100644
index 0000000..e2c684d
--- /dev/null
+++ b/rdfloader/parser2v2/parse_license_test.go
@@ -0,0 +1,853 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "reflect"
+ "sort"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_2_getAnyLicenseFromNode(t *testing.T) {
+ // since this function is a mux, we just have to make sure that with each
+ // type of input, it is able to redirect the request to an appropriate
+ // license getter.
+
+ // TestCase 1: input node is just a node string without any associated
+ // triple (either a NONE|NOASSERTION) because for other case,
+ // the license should've been associated with other triples
+ parser, _ := parserFromBodyContent(``)
+ inputNode := &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NONE",
+ }
+ lic, err := parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a SpecialLicense
+ switch lic.(type) {
+ case SpecialLicense:
+ default:
+ t.Errorf("expected license to be of type SpecialLicense, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 2: DisjunctiveLicenseSet:
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a DisjunctiveLicenseSet
+ switch lic.(type) {
+ case DisjunctiveLicenseSet:
+ default:
+ t.Errorf("expected license to be of type DisjunctiveLicenseSet, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 3: ConjunctiveLicenseSet:
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ConjunctiveLicenseSet
+ switch lic.(type) {
+ case ConjunctiveLicenseSet:
+ default:
+ t.Errorf("expected license to be of type ConjunctiveLicenseSet, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 4: ExtractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.dev/spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[...]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ExtractedLicensingInfo
+ switch lic.(type) {
+ case ExtractedLicensingInfo:
+ default:
+ t.Errorf("expected license to be of type ExtractedLicensingInfo, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 4: ExtractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.dev/spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[...]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ExtractedLicensingInfo
+ switch lic.(type) {
+ case ExtractedLicensingInfo:
+ default:
+ t.Errorf("expected license to be of type ExtractedLicensingInfo, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 5: License
+ parser, _ = parserFromBodyContent(`
+ <spdx:License rdf:about="http://spdx.org/licenses/Apache-2.0">
+ <spdx:standardLicenseTemplate>&lt;&gt; Apache License Version 2.0, January 2004 http://www.apache.org/licenses/&lt;&gt;&lt;&gt; TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION&lt;&gt; &lt;&gt; Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. &lt;&gt; Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. &lt;&gt; Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. &lt;&gt; Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: &lt;&gt; You must give any other recipients of the Work or Derivative Works a copy of this License; and &lt;&gt; You must cause any modified files to carry prominent notices stating that You changed the files; and &lt;&gt; You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and &lt;&gt; If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. &lt;&gt; Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. &lt;&gt; Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. &lt;&gt; Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. &lt;&gt; Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. &lt;&gt; Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.&lt;&gt; END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright &lt;&gt; Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.&lt;&gt;</spdx:standardLicenseTemplate>
+ <rdfs:seeAlso>http://www.apache.org/licenses/LICENSE-2.0</rdfs:seeAlso>
+ <spdx:name>Apache License 2.0</spdx:name>
+ <spdx:licenseId>Apache-2.0</spdx:licenseId>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <rdfs:seeAlso>http://www.opensource.org/licenses/Apache-2.0</rdfs:seeAlso>
+ <spdx:licenseText>...</spdx:licenseText>
+ <spdx:standardLicenseHeader>...</spdx:standardLicenseHeader>
+ </spdx:License>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a License
+ switch lic.(type) {
+ case License:
+ default:
+ t.Errorf("expected license to be of type License, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 5: WithExceptionOperator
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:licenseException>
+ <spdx:LicenseException rdf:nodeID="A1">
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment></rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:WithExceptionOperator>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a WithExceptionOperator
+ switch lic.(type) {
+ case WithExceptionOperator:
+ default:
+ t.Errorf("expected license to be of type WithExceptionOperator, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 6: OrLaterOperator
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a OrLaterOperator
+ switch lic.(type) {
+ case OrLaterOperator:
+ default:
+ t.Errorf("expected license to be of type OrLaterOperator, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 7: checking if an unknown license raises an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:UnknownLicense>
+ <spdx:unknownTag />
+ </spdx:UnknownLicense>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getAnyLicenseFromNode(node)
+ t.Log(err)
+ if err == nil {
+ t.Errorf("should've raised an error for invalid input")
+ }
+
+ // TestCase 8: cyclic dependent license must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet rdf:about="#SPDXRef-RecursiveLicense">
+ <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ <spdx:member>
+ <spdx:ConjunctiveLicenseSet rdf:about="#SPDXRef-RecursiveLicense">
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:member rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-RecursiveLicense"/>
+ </spdx:ConjunctiveLicenseSet>
+ </spdx:member>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getAnyLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to cyclic dependent license. found %v", err)
+ }
+}
+
+func Test_rdfParser2_2_getConjunctiveLicenseSetFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+ var licenseNode *gordfParser.Node
+ var license ConjunctiveLicenseSet
+
+ // TestCase 1: invalid license member
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license member, found <nil>")
+ }
+
+ // TestCase 2: invalid predicate in the licenseSet.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/CC0-1.0"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:unknownTag />
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate found")
+ }
+
+ // TestCase 3: valid example.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing licenseSet: %v", err)
+ }
+ nMembers := len(license.members)
+ if nMembers != 2 {
+ t.Errorf("expected licenseSet to have 2 members, found %d", nMembers)
+ }
+ licenseMembers := mapLicensesToStrings(license.members)
+ expectedLicenseMembers := []string{"LGPL-2.0", "Nokia"}
+ sort.Strings(licenseMembers)
+ if !reflect.DeepEqual(licenseMembers, expectedLicenseMembers) {
+ t.Errorf("expected %v, found %v", expectedLicenseMembers, licenseMembers)
+ }
+}
+
+func Test_rdfParser2_2_getDisjunctiveLicenseSetFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+ var licenseNode *gordfParser.Node
+ var license DisjunctiveLicenseSet
+
+ // TestCase 1: invalid license member
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license member, found <nil>")
+ }
+
+ // TestCase 2: invalid predicate in the licenseSet.
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:unknownTag />
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate found")
+ }
+
+ // TestCase 3: valid example.
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing licenseSet: %v", err)
+ }
+ nMembers := len(license.members)
+ if nMembers != 2 {
+ t.Errorf("expected licenseSet to have 2 members, found %d", nMembers)
+ }
+ licenseMembers := mapLicensesToStrings(license.members)
+ expectedLicenseMembers := []string{"LGPL-2.0", "Nokia"}
+ sort.Strings(licenseMembers)
+ if !reflect.DeepEqual(licenseMembers, expectedLicenseMembers) {
+ t.Errorf("expected %v, found %v", expectedLicenseMembers, licenseMembers)
+ }
+}
+
+func Test_rdfParser2_2_getLicenseExceptionFromNode(t *testing.T) {
+ var licenseException LicenseException
+ var err error
+ var node *gordfParser.Node
+ var parser *rdfParser2_2
+
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <rdfs:seeAlso>see-also</rdfs:seeAlso>
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment></rdfs:comment>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getLicenseExceptionFromNode(node)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid uri for rdfs:seeAlso")
+ }
+
+ // TestCase 2: invalid predicate for licenseException
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:unknown></rdfs:unknown>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getLicenseExceptionFromNode(node)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid predicate")
+ }
+
+ // TestCase 3: everything valid
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <rdfs:seeAlso rdf:resource="http://www.opensource.org/licenses/GPL-3.0"/>
+ <spdx:example>no example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:licenseExceptionText>text</spdx:licenseExceptionText>
+ <spdx:name>name</spdx:name>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ licenseException, err = parser.getLicenseExceptionFromNode(node)
+ if err != nil {
+ t.Fatalf("unexpected error while parsing a valid licenseException")
+ }
+ expectedCrossReference := "http://www.opensource.org/licenses/GPL-3.0"
+ if licenseException.seeAlso != expectedCrossReference {
+ t.Errorf("expected: %s, found: %s", expectedCrossReference, licenseException.seeAlso)
+ }
+ expectedExample := "no example"
+ if licenseException.example != expectedExample {
+ t.Errorf("expected: %s, got: %s", expectedExample, licenseException.example)
+ }
+ if licenseException.licenseExceptionId != "Libtool-exception" {
+ t.Errorf("expected: %s, got: %s", "Libtool-exception", licenseException.licenseExceptionId)
+ }
+ if licenseException.comment != "no comments" {
+ t.Errorf("expected: %s, got: %s", "no comments", licenseException.comment)
+ }
+ if licenseException.licenseExceptionText != "text" {
+ t.Errorf("expected: '%s', got: '%s'", "text", licenseException.licenseExceptionText)
+ }
+ if licenseException.name != "name" {
+ t.Errorf("expected: '%s', got: '%s'", "name", licenseException.name)
+ }
+}
+
+func Test_rdfParser2_2_getLicenseFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var license License
+ var err error
+
+ // TestCase 1: isOsiApproved is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isOsiApproved>no</spdx:isOsiApproved>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isOsiApproved should be a valid boolean type")
+ }
+
+ // TestCase 2: rdf:seeAlso not a valid uri must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <rdfs:seeAlso>uri</rdfs:seeAlso>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating invalid uri for rdfs:seeAlso")
+ }
+
+ // TestCase 3: isDeprecatedLicenseId is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isDeprecatedLicenseId>yes</spdx:isDeprecatedLicenseId>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isDeprecatedLicenseId should be a valid boolean type")
+ }
+
+ // TestCase 4: isFsfLibre is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isFsfLibre>no</spdx:isFsfLibre>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isFsfLibre should be a valid boolean type")
+ }
+
+ // TestCase 5: invalid triple for License:
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:unknown />
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("invalid predicate should've raised an error, got <nil>")
+ }
+
+ // TestCase 5: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-3.0-or-later">
+ <rdfs:seeAlso>http://www.opensource.org/licenses/GPL-3.0</rdfs:seeAlso>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <spdx:licenseText>GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007</spdx:licenseText>
+ <spdx:name>GNU General Public License v3.0 or later</spdx:name>
+ <spdx:standardLicenseHeaderTemplate>...</spdx:standardLicenseHeaderTemplate>
+ <spdx:licenseId>GPL-3.0-or-later</spdx:licenseId>
+ <rdfs:comment>This license was released: 29 June 2007</rdfs:comment>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ <spdx:standardLicenseHeader>...</spdx:standardLicenseHeader>
+ <spdx:standardLicenseTemplate>....</spdx:standardLicenseTemplate>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ expectedSeeAlso := "http://www.opensource.org/licenses/GPL-3.0"
+ if len(license.seeAlso) != 1 {
+ t.Fatalf("expected seeAlso to have 1 element, got %d", len(license.seeAlso))
+ }
+ if license.seeAlso[len(license.seeAlso)-1] != expectedSeeAlso {
+ t.Errorf("expected %s, got %s", expectedSeeAlso, license.seeAlso)
+ }
+ if license.isOsiApproved != true {
+ t.Errorf("expected %t, got %t", true, license.isOsiApproved)
+ }
+ expectedLicenseText := "GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007"
+ if license.licenseText != expectedLicenseText {
+ t.Errorf("expected %s, got %s", expectedSeeAlso, license.licenseText)
+ }
+ expectedName := "GNU General Public License v3.0 or later"
+ if license.name != expectedName {
+ t.Errorf("expected %s, got %s", expectedName, license.name)
+ }
+ expectedstdLicHeader := "..."
+ if license.standardLicenseHeader != expectedstdLicHeader {
+ t.Errorf("expected %s, got %s", expectedstdLicHeader, license.standardLicenseHeader)
+ }
+ expectedLicenseId := "GPL-3.0-or-later"
+ if expectedLicenseId != license.licenseID {
+ t.Errorf("expected %s, got %s", expectedLicenseId, license.licenseID)
+ }
+ expectedLicenseComment := "This license was released: 29 June 2007"
+ if expectedLicenseComment != license.comment {
+ t.Errorf("expected %s, got %s", expectedLicenseComment, license.comment)
+ }
+ expectedstdLicTemplate := "..."
+ if license.standardLicenseHeader != expectedstdLicTemplate {
+ t.Errorf("expected %s, got %s", expectedstdLicTemplate, license.standardLicenseTemplate)
+ }
+ expectedstdLicHeaderTemplate := "..."
+ if license.standardLicenseHeaderTemplate != expectedstdLicHeaderTemplate {
+ t.Errorf("expected %s, got %s", expectedstdLicHeaderTemplate, license.standardLicenseHeaderTemplate)
+ }
+ if license.isFsfLibre != true {
+ t.Errorf("expected %t, got %t", true, license.isFsfLibre)
+ }
+}
+
+func Test_rdfParser2_2_getOrLaterOperatorFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: more than one member in the OrLaterOperator tag must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to more than one members, got <nil>")
+ }
+
+ // TestCase 2: Invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:members>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:members>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 5: invalid member
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:invalidTag />
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error parsing invalid license member, got %v", err)
+ }
+
+ // TestCase 4: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid input: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_getSimpleLicensingInfoFromNode(t *testing.T) {
+ // nothing to test. The just provides an interface to call function that
+ // uses triples to render a SimpleLicensingInfo.
+ parser, _ := parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ _, err := parser.getSimpleLicensingInfoFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_getSimpleLicensingInfoFromTriples(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+ var license SimpleLicensingInfo
+
+ // TestCase 1: invalid rdf:seeAlso attribute
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:seeAlso>an invalid uri</rdfs:seeAlso>
+ </spdx:SimpleLicensingInfo>
+ `)
+ _, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err == nil {
+ t.Error("expected an error reporting invalid uri for rdf:seeAlso, got <nil>")
+ }
+
+ // TestCase 2: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:invalidPredicate />
+ </spdx:SimpleLicensingInfo>
+ `)
+ _, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err == nil {
+ t.Error("expected an error reporting invalid predicate, got <nil>")
+ }
+
+ // TestCase 3: valid example
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:comment>comment</rdfs:comment>
+ <spdx:licenseId>lid</spdx:licenseId>
+ <spdx:name>name</spdx:name>
+ <rdfs:seeAlso>https://opensource.org/licenses/MPL-1.0</rdfs:seeAlso>
+ <spdx:example>example</spdx:example>
+ </spdx:SimpleLicensingInfo>
+ `)
+ license, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedComment := "comment"
+ expectedLicenseId := "lid"
+ expectedName := "name"
+ expectedSeeAlso := "https://opensource.org/licenses/MPL-1.0"
+ expectedExample := "example"
+ if expectedComment != license.comment {
+ t.Errorf("expected %v, got %v", expectedComment, license.comment)
+ }
+ if expectedLicenseId != license.licenseID {
+ t.Errorf("expected %v, got %v", expectedLicenseId, license.licenseID)
+ }
+ if expectedName != license.name {
+ t.Errorf("expected %v, got %v", expectedName, license.name)
+ }
+ if len(license.seeAlso) != 1 {
+ t.Fatalf("expected seeAlso to have 1 element, found %d", len(license.seeAlso))
+ }
+ if license.seeAlso[0] != expectedSeeAlso {
+ t.Errorf("expected %v, got %v", expectedSeeAlso, license.seeAlso[0])
+ }
+ if license.example != expectedExample {
+ t.Errorf("expected %v, got %v", expectedExample, license.example)
+ }
+}
+
+func Test_rdfParser2_2_getSpecialLicenseFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var license SpecialLicense
+
+ // TestCase 1: NONE
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NONE",
+ }
+ license, err := parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "NONE" {
+ t.Errorf("expected %s, got %s", "NONE", license.value)
+ }
+
+ // TestCase 2: NOASSERTION
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NOASSERTION",
+ }
+ license, err = parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", license.value)
+ }
+
+ // TestCase 4: undefined standard license
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "https://opensource.org/licenses/unknown",
+ }
+ _, err = parser.getSpecialLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license")
+ }
+
+ // TestCase 4: valid standard license
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "https://opensource.org/licenses/MPL-1.0",
+ }
+ license, err = parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "MPL-1.0" {
+ t.Errorf("expected %s, got %s", "MPL-1.0", license.value)
+ }
+}
+
+func Test_rdfParser2_2_getWithExceptionOperatorFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: more than one member in the OrLaterOperator tag must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to more than one members, got <nil>")
+ }
+
+ // TestCase 2: Invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:members>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:members>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 3: Invalid member
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later">
+ <spdx:unknownTag />
+ </spdx:License>
+ </spdx:member>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to error parsing a member, got <nil>")
+ }
+
+ // TestCase 4: Invalid licenseException
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:member>
+ <spdx:licenseException>
+ <spdx:LicenseException>
+ <spdx:invalidTag />
+ <spdx:example>example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid licenseException, got <nil>")
+ }
+
+ // TestCase 5: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:member>
+ <spdx:licenseException>
+ <spdx:LicenseException>
+ <spdx:example>example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_other_license_info.go b/rdfloader/parser2v2/parse_other_license_info.go
new file mode 100644
index 0000000..97dcf0c
--- /dev/null
+++ b/rdfloader/parser2v2/parse_other_license_info.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *rdfParser2_2) getExtractedLicensingInfoFromNode(node *gordfParser.Node) (lic ExtractedLicensingInfo, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var restTriples []*gordfParser.Triple
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case SPDX_EXTRACTED_TEXT:
+ lic.extractedText = triple.Object.ID
+ default:
+ restTriples = append(restTriples, triple)
+ }
+ }
+ lic.SimpleLicensingInfo, err = parser.getSimpleLicensingInfoFromTriples(restTriples)
+ if err != nil {
+ return lic, fmt.Errorf("error setting simple licensing information of extracted licensing info: %s", err)
+ }
+ return lic, nil
+}
+
+func (parser *rdfParser2_2) extractedLicenseToOtherLicense(extLicense ExtractedLicensingInfo) (othLic v2_2.OtherLicense) {
+ othLic.LicenseIdentifier = extLicense.licenseID
+ othLic.ExtractedText = extLicense.extractedText
+ othLic.LicenseComment = extLicense.comment
+ othLic.LicenseCrossReferences = extLicense.seeAlso
+ othLic.LicenseName = extLicense.name
+ return othLic
+}
diff --git a/rdfloader/parser2v2/parse_other_license_info_test.go b/rdfloader/parser2v2/parse_other_license_info_test.go
new file mode 100644
index 0000000..cd7ad32
--- /dev/null
+++ b/rdfloader/parser2v2/parse_other_license_info_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_2_getExtractedLicensingInfoFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseID>LicenseRef-Freeware</spdx:licenseID>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExtractedLicensingInfoFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate, got <nil>")
+ }
+
+ // TestCase 2: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExtractedLicensingInfoFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_extractedLicenseToOtherLicense(t *testing.T) {
+ // nothing to test for this function.
+ parser, _ := parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ extLicense, _ := parser.getExtractedLicensingInfoFromNode(node)
+ othLic := parser.extractedLicenseToOtherLicense(extLicense)
+
+ if othLic.LicenseIdentifier != extLicense.licenseID {
+ t.Errorf("expected %v, got %v", othLic.LicenseIdentifier, extLicense.licenseID)
+ }
+ if othLic.ExtractedText != extLicense.extractedText {
+ t.Errorf("expected %v, got %v", othLic.ExtractedText, extLicense.extractedText)
+ }
+ if othLic.LicenseComment != extLicense.comment {
+ t.Errorf("expected %v, got %v", othLic.LicenseComment, extLicense.comment)
+ }
+ if !reflect.DeepEqual(othLic.LicenseCrossReferences, extLicense.seeAlso) {
+ t.Errorf("expected %v, got %v", othLic.LicenseCrossReferences, extLicense.seeAlso)
+ }
+ if othLic.LicenseName != extLicense.name {
+ t.Errorf("expected %v, got %v", othLic.LicenseName, extLicense.name)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_package.go b/rdfloader/parser2v2/parse_package.go
new file mode 100644
index 0000000..e6d9f60
--- /dev/null
+++ b/rdfloader/parser2v2/parse_package.go
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *rdfParser2_2) getPackageFromNode(packageNode *gordfParser.Node) (pkg *v2_2.Package, err error) {
+ pkg = &v2_2.Package{} // new package which will be returned
+
+ currState := parser.cache[packageNode.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[packageNode.ID] = &nodeState{
+ object: pkg,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this package node and we needn't parse it again.
+ return currState.object.(*v2_2.Package), nil
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[packageNode.ID].Color = GREY
+
+ // setting state color to black to indicate when we're done parsing this node.
+ defer func() { parser.cache[packageNode.ID].Color = BLACK }()
+
+ // setting the SPDXIdentifier for the package.
+ eId, err := ExtractElementID(getLastPartOfURI(packageNode.ID))
+ if err != nil {
+ return nil, fmt.Errorf("error extracting elementID of a package identifier: %v", err)
+ }
+ pkg.PackageSPDXIdentifier = eId // 3.2
+
+ // check if we already have a package initialized for this ID
+ existingPackageIndex := -1
+ for ii, existingPkg := range parser.doc.Packages {
+ if existingPkg != nil && existingPkg.PackageSPDXIdentifier == eId {
+ existingPackageIndex = ii
+ pkg = existingPkg
+ break
+ }
+ }
+
+ // iterate over all the triples associated with the provided package packageNode.
+ for _, subTriple := range parser.nodeToTriples(packageNode) {
+ switch subTriple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case SPDX_NAME: // 3.1
+ // cardinality: exactly 1
+ pkg.PackageName = subTriple.Object.ID
+ case SPDX_VERSION_INFO: // 3.3
+ // cardinality: max 1
+ pkg.PackageVersion = subTriple.Object.ID
+ case SPDX_PACKAGE_FILE_NAME: // 3.4
+ // cardinality: max 1
+ pkg.PackageFileName = subTriple.Object.ID
+ case SPDX_SUPPLIER: // 3.5
+ // cardinality: max 1
+ err = setPackageSupplier(pkg, subTriple.Object.ID)
+ case SPDX_ORIGINATOR: // 3.6
+ // cardinality: max 1
+ err = setPackageOriginator(pkg, subTriple.Object.ID)
+ case SPDX_DOWNLOAD_LOCATION: // 3.7
+ // cardinality: exactly 1
+ err = setDocumentLocationFromURI(pkg, subTriple.Object.ID)
+ case SPDX_FILES_ANALYZED: // 3.8
+ // cardinality: max 1
+ err = setFilesAnalyzed(pkg, subTriple.Object.ID)
+ case SPDX_PACKAGE_VERIFICATION_CODE: // 3.9
+ // cardinality: max 1
+ err = parser.setPackageVerificationCode(pkg, subTriple.Object)
+ case SPDX_CHECKSUM: // 3.10
+ // cardinality: min 0
+ err = parser.setPackageChecksum(pkg, subTriple.Object)
+ case DOAP_HOMEPAGE: // 3.11
+ // cardinality: max 1
+ // homepage must be a valid Uri
+ if !isUriValid(subTriple.Object.ID) {
+ return nil, fmt.Errorf("invalid uri %s while parsing doap_homepage in a package", subTriple.Object.ID)
+ }
+ pkg.PackageHomePage = subTriple.Object.ID
+ case SPDX_SOURCE_INFO: // 3.12
+ // cardinality: max 1
+ pkg.PackageSourceInfo = subTriple.Object.ID
+ case SPDX_LICENSE_CONCLUDED: // 3.13
+ // cardinality: exactly 1
+ anyLicenseInfo, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ pkg.PackageLicenseConcluded = anyLicenseInfo.ToLicenseString()
+ case SPDX_LICENSE_INFO_FROM_FILES: // 3.14
+ // cardinality: min 0
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, getLicenseStringFromURI(subTriple.Object.ID))
+ case SPDX_LICENSE_DECLARED: // 3.15
+ // cardinality: exactly 1
+ anyLicenseInfo, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ pkg.PackageLicenseDeclared = anyLicenseInfo.ToLicenseString()
+ case SPDX_LICENSE_COMMENTS: // 3.16
+ // cardinality: max 1
+ pkg.PackageLicenseComments = subTriple.Object.ID
+ case SPDX_COPYRIGHT_TEXT: // 3.17
+ // cardinality: exactly 1
+ pkg.PackageCopyrightText = subTriple.Object.ID
+ case SPDX_SUMMARY: // 3.18
+ // cardinality: max 1
+ pkg.PackageSummary = subTriple.Object.ID
+ case SPDX_DESCRIPTION: // 3.19
+ // cardinality: max 1
+ pkg.PackageDescription = subTriple.Object.ID
+ case RDFS_COMMENT: // 3.20
+ // cardinality: max 1
+ pkg.PackageComment = subTriple.Object.ID
+ case SPDX_EXTERNAL_REF: // 3.21
+ // cardinality: min 0
+ externalDocRef, err := parser.getPackageExternalRef(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing externalRef of a package: %v", err)
+ }
+ pkg.PackageExternalReferences = append(pkg.PackageExternalReferences, externalDocRef)
+ case SPDX_HAS_FILE: // 3.22
+ // cardinality: min 0
+ file, err := parser.getFileFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error setting file inside a package: %v", err)
+ }
+ parser.setFileToPackage(pkg, file)
+ case SPDX_RELATIONSHIP:
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ case SPDX_ATTRIBUTION_TEXT:
+ // cardinality: min 0
+ pkg.PackageAttributionTexts = append(pkg.PackageAttributionTexts, subTriple.Object.ID)
+ case SPDX_ANNOTATION:
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ default:
+ return nil, fmt.Errorf("unknown predicate id %s while parsing a package", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if existingPackageIndex != -1 {
+ parser.doc.Packages[existingPackageIndex] = pkg
+ } else {
+ parser.doc.Packages = append(parser.doc.Packages, pkg)
+ }
+
+ return pkg, nil
+}
+
+// parses externalReference found in the package by the associated triple.
+func (parser *rdfParser2_2) getPackageExternalRef(node *gordfParser.Node) (externalDocRef *v2_2.PackageExternalReference, err error) {
+ externalDocRef = &v2_2.PackageExternalReference{}
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_REFERENCE_CATEGORY:
+ // cardinality: exactly 1
+ switch triple.Object.ID {
+ case SPDX_REFERENCE_CATEGORY_SECURITY:
+ externalDocRef.Category = "SECURITY"
+ case SPDX_REFERENCE_CATEGORY_PACKAGE_MANAGER:
+ externalDocRef.Category = "PACKAGE-MANAGER"
+ case SPDX_REFERENCE_CATEGORY_OTHER:
+ externalDocRef.Category = "OTHER"
+ default:
+ return nil, fmt.Errorf("unknown packageManager uri %s", triple.Predicate.ID)
+ }
+ case RDF_TYPE:
+ continue
+ case SPDX_REFERENCE_TYPE:
+ // assumes: the reference type is associated with just the uri and
+ // other associated fields are ignored.
+ // other fields include:
+ // 1. contextualExample,
+ // 2. documentation and,
+ // 3. externalReferenceSite
+ externalDocRef.RefType = triple.Object.ID
+ case SPDX_REFERENCE_LOCATOR:
+ // cardinality: exactly 1
+ externalDocRef.Locator = triple.Object.ID
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ externalDocRef.ExternalRefComment = triple.Object.ID
+ default:
+ return nil, fmt.Errorf("unknown package external reference predicate id %s", triple.Predicate.ID)
+ }
+ }
+ return
+}
+
+func (parser *rdfParser2_2) setPackageVerificationCode(pkg *v2_2.Package, node *gordfParser.Node) error {
+ for _, subTriple := range parser.nodeToTriples(node) {
+ switch subTriple.Predicate.ID {
+ case SPDX_PACKAGE_VERIFICATION_CODE_VALUE:
+ // cardinality: exactly 1
+ pkg.PackageVerificationCode.Value = subTriple.Object.ID
+ case SPDX_PACKAGE_VERIFICATION_CODE_EXCLUDED_FILE:
+ // cardinality: min 0
+ pkg.PackageVerificationCode.ExcludedFiles = append(pkg.PackageVerificationCode.ExcludedFiles, subTriple.Object.ID)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ default:
+ return fmt.Errorf("unparsed predicate %s", subTriple.Predicate.ID)
+ }
+ }
+ return nil
+}
+
+// appends the file to the package and also sets the assocWithPackage for the
+// file to indicate the file is associated with a package
+func (parser *rdfParser2_2) setFileToPackage(pkg *v2_2.Package, file *v2_2.File) {
+ if pkg.Files == nil {
+ pkg.Files = []*v2_2.File{}
+ }
+ pkg.Files = append(pkg.Files, file)
+ parser.assocWithPackage[file.FileSPDXIdentifier] = true
+}
+
+// given a supplierObject, sets the PackageSupplier attribute of the pkg.
+// Args:
+//
+// value: [NOASSERTION | [Person | Organization]: string]
+func setPackageSupplier(pkg *v2_2.Package, value string) error {
+ value = strings.TrimSpace(value)
+ supplier := &common.Supplier{}
+ if strings.ToUpper(value) == "NOASSERTION" {
+ supplier.Supplier = "NOASSERTION"
+ pkg.PackageSupplier = supplier
+ return nil
+ }
+
+ subKey, subValue, err := ExtractSubs(value, ":")
+ if err != nil {
+ return fmt.Errorf("package supplier must be of the form NOASSERTION or [Person|Organization]: string. found: %s", value)
+ }
+ switch subKey {
+ case "Person", "Organization":
+ supplier.Supplier = subValue
+ supplier.SupplierType = subKey
+ default:
+ return fmt.Errorf("unknown supplier %s", subKey)
+ }
+
+ pkg.PackageSupplier = supplier
+
+ return nil
+}
+
+// given a OriginatorObject, sets the PackageOriginator attribute of the pkg.
+// Args:
+//
+// value: [NOASSERTION | [Person | Organization]: string]
+func setPackageOriginator(pkg *v2_2.Package, value string) error {
+ value = strings.TrimSpace(value)
+ originator := &common.Originator{}
+ if strings.ToUpper(value) == "NOASSERTION" {
+ originator.Originator = "NOASSERTION"
+ pkg.PackageOriginator = originator
+ return nil
+ }
+
+ subKey, subValue, err := ExtractSubs(value, ":")
+ if err != nil {
+ return fmt.Errorf("package Originator must be of the form NOASSERTION or [Person|Organization]: string. found: %s", value)
+ }
+ switch subKey {
+ case "Person", "Organization":
+ originator.Originator = subValue
+ originator.OriginatorType = subKey
+ default:
+ return fmt.Errorf("unknown Originator %s", subKey)
+ }
+
+ pkg.PackageOriginator = originator
+
+ return nil
+}
+
+// validates the uri and sets the location if it is valid
+func setDocumentLocationFromURI(pkg *v2_2.Package, locationURI string) error {
+ switch locationURI {
+ case SPDX_NOASSERTION_CAPS, SPDX_NOASSERTION_SMALL:
+ pkg.PackageDownloadLocation = "NOASSERTION"
+ case SPDX_NONE_CAPS, SPDX_NONE_SMALL:
+ pkg.PackageDownloadLocation = "NONE"
+ default:
+ if !isUriValid(locationURI) {
+ return fmt.Errorf("%s is not a valid uri", locationURI)
+ }
+ pkg.PackageDownloadLocation = locationURI
+ }
+ return nil
+}
+
+// sets the FilesAnalyzed attribute to the given package
+// boolValue is a string of type "true" or "false"
+func setFilesAnalyzed(pkg *v2_2.Package, boolValue string) (err error) {
+ pkg.IsFilesAnalyzedTagPresent = true
+ pkg.FilesAnalyzed, err = boolFromString(boolValue)
+ return err
+}
+
+func (parser *rdfParser2_2) setPackageChecksum(pkg *v2_2.Package, node *gordfParser.Node) error {
+ checksumAlgorithm, checksumValue, err := parser.getChecksumFromNode(node)
+ if err != nil {
+ return fmt.Errorf("error getting checksum algorithm and value from %v", node)
+ }
+ if pkg.PackageChecksums == nil {
+ pkg.PackageChecksums = make([]common.Checksum, 0, 1)
+ }
+ switch checksumAlgorithm {
+ case common.SHA1,
+ common.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6:
+ pkg.PackageChecksums = append(pkg.PackageChecksums, common.Checksum{Algorithm: checksumAlgorithm, Value: checksumValue})
+ default:
+ return fmt.Errorf("unknown checksumAlgorithm %s while parsing a package", checksumAlgorithm)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v2/parse_package_test.go b/rdfloader/parser2v2/parse_package_test.go
new file mode 100644
index 0000000..f458984
--- /dev/null
+++ b/rdfloader/parser2v2/parse_package_test.go
@@ -0,0 +1,768 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func Test_setPackageSupplier(t *testing.T) {
+ var err error
+
+ // TestCase 1: no assertion must set PackageSupplierNOASSERTION field to true
+ pkg := &v2_2.Package{}
+ err = setPackageSupplier(pkg, "NOASSERTION")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("PackageSupplier must've been set to NOASSERTION")
+ }
+
+ // TestCase 2: lower-case noassertion must also set the
+ // PackageSupplierNOASSERTION to true.
+ pkg = &v2_2.Package{}
+ err = setPackageSupplier(pkg, "noassertion")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("PackageSupplier must've been set to NOASSERTION")
+ }
+
+ // TestCase 3: invalid input without colon separator. must raise an error
+ pkg = &v2_2.Package{}
+ input := "string without colon separator"
+ err = setPackageSupplier(pkg, input)
+ if err == nil {
+ t.Errorf("invalid input \"%s\" didn't raise an error", input)
+ }
+
+ // TestCase 4: Valid Person
+ pkg = &v2_2.Package{}
+ personName := "Rishabh Bhatnagar"
+ input = "Person: " + personName
+ err = setPackageSupplier(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != personName {
+ t.Errorf("PackageSupplierPerson should be %s. found %s", personName, pkg.PackageSupplier.Supplier)
+ }
+
+ // TestCase 5: Valid Organization
+ pkg = &v2_2.Package{}
+ orgName := "SPDX"
+ input = "Organization: " + orgName
+ err = setPackageSupplier(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != orgName {
+ t.Errorf("PackageSupplierPerson should be %s. found %s", orgName, pkg.PackageSupplier.Supplier)
+ }
+
+ // TestCase 6: Invalid EntityType
+ pkg = &v2_2.Package{}
+ input = "InvalidEntity: entity"
+ err = setPackageSupplier(pkg, input)
+ if err == nil {
+ t.Errorf("invalid entity should've raised an error")
+ }
+}
+
+func Test_setPackageOriginator(t *testing.T) {
+ var err error
+
+ // TestCase 1: no assertion must set PackageSupplierNOASSERTION field to true
+ pkg := &v2_2.Package{}
+ err = setPackageOriginator(pkg, "NOASSERTION")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("PackageOriginator must've been set to NOASSERTION")
+ }
+
+ // TestCase 2: lower-case noassertion must also set the
+ // PackageOriginatorNOASSERTION to true.
+ pkg = &v2_2.Package{}
+ err = setPackageOriginator(pkg, "noassertion")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("PackageOriginator must've been set to NOASSERTION")
+ }
+
+ // TestCase 3: invalid input without colon separator. must raise an error
+ pkg = &v2_2.Package{}
+ input := "string without colon separator"
+ err = setPackageOriginator(pkg, input)
+ if err == nil {
+ t.Errorf("invalid input \"%s\" didn't raise an error", input)
+ }
+
+ // TestCase 4: Valid Person
+ pkg = &v2_2.Package{}
+ personName := "Rishabh Bhatnagar"
+ input = "Person: " + personName
+ err = setPackageOriginator(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != personName {
+ t.Errorf("PackageOriginatorPerson should be %s. found %s", personName, pkg.PackageOriginator.Originator)
+ }
+
+ // TestCase 5: Valid Organization
+ pkg = &v2_2.Package{}
+ orgName := "SPDX"
+ input = "Organization: " + orgName
+ err = setPackageOriginator(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != orgName {
+ t.Errorf("PackageOriginatorOrganization should be %s. found %s", orgName, pkg.PackageOriginator.Originator)
+ }
+
+ // TestCase 6: Invalid EntityType
+ pkg = &v2_2.Package{}
+ input = "InvalidEntity: entity"
+ err = setPackageOriginator(pkg, input)
+ if err == nil {
+ t.Errorf("invalid entity should've raised an error")
+ }
+}
+
+func Test_rdfParser2_2_setPackageVerificationCode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var pkg *v2_2.Package
+ var err error
+
+ // TestCase 1: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx.PackageVerificationCode>
+ <spdx:invalidPredicate />
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg = &v2_2.Package{}
+ err = parser.setPackageVerificationCode(pkg, node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 2: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx.PackageVerificationCode>
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg = &v2_2.Package{}
+ err = parser.setPackageVerificationCode(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedValue := "cbceb8b5689b75a584efe35587b5d41bd48820ce"
+ if pkg.PackageVerificationCode.Value != expectedValue {
+ t.Errorf("expected %v, got %v", expectedValue, pkg.PackageVerificationCode)
+ }
+ expectedExcludedFile := "./package.spdx"
+ if pkg.PackageVerificationCode.ExcludedFiles[0] != expectedExcludedFile {
+ t.Errorf("expected %v, got %v", expectedExcludedFile, pkg.PackageVerificationCode.ExcludedFiles)
+ }
+}
+
+func Test_rdfParser2_2_getPackageExternalRef(t *testing.T) {
+ var extRef *v2_2.PackageExternalReference
+ var err error
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid reference category
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_invalid"/>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid referenceCategory, got <nil>")
+ }
+
+ // TestCase 2: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:unknownPredicate />
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid referenceCategory, got <nil>")
+ }
+
+ // TestCase 3: valid example (referenceCategory_security)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef := &v2_2.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "SECURITY",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+
+ // TestCase 4: valid example (referenceCategory_packageManager)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_packageManager"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef = &v2_2.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "PACKAGE-MANAGER",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+
+ // TestCase 5: valid example (referenceCategory_other)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_other"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef = &v2_2.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "OTHER",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+}
+
+func Test_rdfParser2_2_getPackageFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid elementId
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#upload2">
+ <spdx:name>time-1.9.tar.gz</spdx:name>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(missing SPDXRef- prefix), found %v", err)
+ }
+
+ // TestCase 2: Invalid License Concluded must raise an error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/IPL-3.0"/>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid license), found %v", err)
+ }
+
+ // TestCase 2: Invalid License Declared must raise an error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/licenses/IPL-3.0"/>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid license), found %v", err)
+ }
+
+ // TestCase 3: Invalid ExternalRef
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_invalid"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid externalRef), found %v", err)
+ }
+
+ // TestCase 4: invalid file must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#item8"/>
+ </spdx:hasFile>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid file), found %v", err)
+ }
+
+ // TestCase 5: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:hasFiles>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#item8"/>
+ </spdx:hasFiles>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid predicate), found %v", err)
+ }
+
+ // TestCase 6: invalid annotation must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid annotation), found %v", err)
+ }
+
+ // TestCase 6: invalid homepage must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <doap:homepage>u r i</doap:homepage>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid homepage uri), found %v", err)
+ }
+
+ // TestCase 7: Package tag declared more than once should be parsed into a single object's definition
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ yetAnotherPkgTriple := gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_PACKAGE_FILE_NAME,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.LITERAL,
+ ID: "packageFileName",
+ },
+ }
+ parser.nodeStringToTriples[node.String()] = append(parser.nodeStringToTriples[node.String()], &yetAnotherPkgTriple)
+ pkg, err := parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ // validating if all the attributes that spanned over two tags are included in the parsed package.
+ expectedID := "upload2"
+ if string(pkg.PackageSPDXIdentifier) != expectedID {
+ t.Errorf("expected package id: %s, got %s", expectedID, pkg.PackageSPDXIdentifier)
+ }
+ expectedPkgFileName := "packageFileName"
+ if expectedPkgFileName != pkg.PackageFileName {
+ t.Errorf("expected package file name: %s, got %s", expectedPkgFileName, pkg.PackageFileName)
+ }
+ expectedName := "Test Package"
+ if pkg.PackageName != expectedName {
+ t.Errorf("expected package name: %s, got %s", expectedPkgFileName, pkg.PackageName)
+ }
+
+ // TestCase 8: Checking if packages can handle cyclic dependencies:
+ // Simulating a smallest possible cycle: package related to itself.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes" />
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:versionInfo>1.1.1</spdx:versionInfo>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ // checking if both the attributes of the packages are set.
+ expectedVersionInfo := "1.1.1"
+ expectedPackageName := "Test Package"
+ if pkg.PackageVersion != expectedVersionInfo {
+ t.Errorf("Expected %s, found %s", expectedVersionInfo, pkg.PackageVersion)
+ }
+ if pkg.PackageName != expectedPackageName {
+ t.Errorf("Expected %s, found %s", expectedPackageName, pkg.PackageName)
+ }
+
+ // TestCase 9: everything valid
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ <spdx:versionInfo>1.1.1</spdx:versionInfo>
+ <spdx:packageFileName>time-1.9.tar.gz</spdx:packageFileName>
+ <spdx:supplier>Person: Jane Doe (jane.doe@example.com)</spdx:supplier>
+ <spdx:originator>Organization: SPDX</spdx:originator>
+ <spdx:downloadLocation rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:filesAnalyzed>true</spdx:filesAnalyzed>
+ <spdx:packageVerificationCode>
+ <spdx.PackageVerificationCode>
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ </spdx:packageVerificationCode>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>75068c26abbed3ad3980685bae21d7202d288317</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <spdx:sourceInfo>uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.</spdx:sourceInfo>
+ <spdx:licenseConcluded>
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ </spdx:licenseConcluded>
+ <spdx:licenseInfoFromFiles rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseComments>Other versions available for a commercial license</spdx:licenseComments>
+ <spdx:copyrightText rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:summary> Package for Testing </spdx:summary>
+ <spdx:description> Some tags are taken from other spdx autogenerated files </spdx:description>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ <spdx:hasFile rdf:resource="http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-129" />
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes" />
+ <spdx:relatedSpdxElement rdf:resource="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2" />
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:attributionText>attribution text</spdx:attributionText>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Package level annotation</rdfs:comment>
+ <spdx:annotator>Person: Package Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_setFileToPackage(t *testing.T) {
+ var pkg *v2_2.Package
+ var file *v2_2.File
+ var parser *rdfParser2_2
+
+ // TestCase 1: setting to a nil files attribute shouldn't panic.
+ parser, _ = parserFromBodyContent(``)
+ pkg = &v2_2.Package{}
+ file = &v2_2.File{}
+ parser.setFileToPackage(pkg, file)
+ if len(pkg.Files) != 1 {
+ t.Errorf("expected given package to have one file after setting, got %d", len(pkg.Files))
+ }
+ if parser.assocWithPackage[file.FileSPDXIdentifier] != true {
+ t.Errorf("given file should've been associated with a package, assocWithPackage is false")
+ }
+}
+
+func Test_rdfParser2_2_setPackageChecksum(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var pkg *v2_2.Package
+ var expectedChecksumValue string
+ var err error
+
+ // TestCase 1: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_2.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err == nil {
+ t.Error("expected an error due to invalid checksum node, got <nil>")
+ }
+
+ // TestCase 1: valid checksum algorithm which is invalid for package
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha2000"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_2.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err == nil {
+ t.Error("expected an error due to invalid checksum for package, got <nil>")
+ }
+
+ // TestCase 2: valid checksum (sha1)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_2.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 3: valid checksum (sha256)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_2.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA256:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 4: valid checksum (md5)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_2.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.MD5:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+}
+
+func Test_setDocumentLocationFromURI(t *testing.T) {
+ var pkg *v2_2.Package
+ var expectedDocumentLocation, gotDocumentLocation string
+ var inputURI string
+ var err error
+
+ // TestCase 1: NOASSERTION
+ inputURI = SPDX_NOASSERTION_SMALL
+ pkg = &v2_2.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "NOASSERTION"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 2: NONE
+ inputURI = SPDX_NONE_CAPS
+ pkg = &v2_2.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "NONE"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 3: valid uri
+ inputURI = "https://www.gnu.org/software/texinfo/"
+ pkg = &v2_2.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "https://www.gnu.org/software/texinfo/"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 3: invalid uri
+ inputURI = " "
+ pkg = &v2_2.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err == nil {
+ t.Fatalf("expected an error due to invalid uri, got %v", err)
+ }
+}
+
+func Test_setFilesAnalyzed(t *testing.T) {
+ var pkg *v2_2.Package
+ var err error
+
+ // TestCase 1: not a valid bool value:
+ pkg = &v2_2.Package{}
+ err = setFilesAnalyzed(pkg, "no")
+ if err == nil {
+ t.Errorf("expected an error due to invalid bool input, got %v", err)
+ }
+
+ // TestCase 2: valid input
+ pkg = &v2_2.Package{}
+ err = setFilesAnalyzed(pkg, "true")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !pkg.IsFilesAnalyzedTagPresent {
+ t.Errorf("should've set IsFilesAnalyzedTagPresent, got: %t", pkg.IsFilesAnalyzedTagPresent)
+ }
+ if !pkg.FilesAnalyzed {
+ t.Errorf("expected: %t, got: %t", true, pkg.FilesAnalyzed)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_relationship.go b/rdfloader/parser2v2/parse_relationship.go
new file mode 100644
index 0000000..49f1025
--- /dev/null
+++ b/rdfloader/parser2v2/parse_relationship.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// parsing the relationship that exists in the rdf document.
+// Relationship is of type RefA relationType RefB.
+// parsing the relationship appends the relationship to the current document's
+// Relationships Slice.
+func (parser *rdfParser2_2) parseRelationship(triple *gordfParser.Triple) (err error) {
+ reln := v2_2.Relationship{}
+
+ reln.RefA, err = getReferenceFromURI(triple.Subject.ID)
+ if err != nil {
+ return err
+ }
+
+ currState := parser.cache[triple.Object.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[triple.Object.ID] = &nodeState{
+ object: reln,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this relationship node and we needn't parse it again.
+ return nil
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[triple.Object.ID].Color = GREY
+
+ // setting state color to black to indicate when we're done parsing this node.
+ defer func() { parser.cache[triple.Object.ID].Color = BLACK }()
+
+ for _, subTriple := range parser.nodeToTriples(triple.Object) {
+ switch subTriple.Predicate.ID {
+ case SPDX_RELATIONSHIP_TYPE:
+ // cardinality: exactly 1
+ reln.Relationship, err = getRelationshipTypeFromURI(subTriple.Object.ID)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case SPDX_RELATED_SPDX_ELEMENT:
+ // cardinality: exactly 1
+ // assumes: spdx-element is a uri
+ reln.RefB, err = getReferenceFromURI(subTriple.Object.ID)
+ if err != nil {
+ return err
+ }
+
+ relatedSpdxElementTriples := parser.nodeToTriples(subTriple.Object)
+ if len(relatedSpdxElementTriples) == 0 {
+ continue
+ }
+
+ typeTriples := rdfwriter.FilterTriples(relatedSpdxElementTriples, &subTriple.Object.ID, &RDF_TYPE, nil)
+ if len(typeTriples) != 1 {
+ return fmt.Errorf("expected %s to have exactly one rdf:type triple. found %d triples", subTriple.Object, len(typeTriples))
+ }
+ err = parser.parseRelatedElementFromTriple(&reln, typeTriples[0])
+ if err != nil {
+ return err
+ }
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ reln.RelationshipComment = subTriple.Object.ID
+ default:
+ return fmt.Errorf("unexpected predicate id: %s", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ parser.doc.Relationships = append(parser.doc.Relationships, &reln)
+ return nil
+}
+
+func (parser *rdfParser2_2) parseRelatedElementFromTriple(reln *v2_2.Relationship, triple *gordfParser.Triple) error {
+ // iterate over relatedElement Type and check which SpdxElement it is.
+ var err error
+ switch triple.Object.ID {
+ case SPDX_FILE:
+ file, err := parser.getFileFromNode(triple.Subject)
+ if err != nil {
+ return fmt.Errorf("error setting a file: %v", err)
+ }
+ reln.RefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: file.FileSPDXIdentifier,
+ }
+
+ case SPDX_PACKAGE:
+ pkg, err := parser.getPackageFromNode(triple.Subject)
+ if err != nil {
+ return fmt.Errorf("error setting a package inside a relationship: %v", err)
+ }
+ reln.RefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: pkg.PackageSPDXIdentifier,
+ }
+
+ case SPDX_SPDX_ELEMENT:
+ // it shouldn't be associated with any other triple.
+ // it must be a uri reference.
+ reln.RefB, err = ExtractDocElementID(getLastPartOfURI(triple.Subject.ID))
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("undefined relatedElement %s found while parsing relationship", triple.Object.ID)
+ }
+ return nil
+}
+
+// references like RefA and RefB of any relationship
+func getReferenceFromURI(uri string) (common.DocElementID, error) {
+ fragment := getLastPartOfURI(uri)
+ switch strings.ToLower(strings.TrimSpace(fragment)) {
+ case "noassertion", "none":
+ return common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: common.ElementID(strings.ToUpper(fragment)),
+ }, nil
+ }
+ return ExtractDocElementID(fragment)
+}
+
+// note: relationshipType is case sensitive.
+func getRelationshipTypeFromURI(relnTypeURI string) (string, error) {
+ relnTypeURI = strings.TrimSpace(relnTypeURI)
+ lastPart := getLastPartOfURI(relnTypeURI)
+ if !strings.HasPrefix(lastPart, PREFIX_RELATIONSHIP_TYPE) {
+ return "", fmt.Errorf("relationshipType must start with %s. found %s", PREFIX_RELATIONSHIP_TYPE, lastPart)
+ }
+ lastPart = strings.TrimPrefix(lastPart, PREFIX_RELATIONSHIP_TYPE)
+
+ lastPart = strings.TrimSpace(lastPart)
+ for _, validRelationshipType := range AllRelationshipTypes() {
+ if lastPart == validRelationshipType {
+ return lastPart, nil
+ }
+ }
+ return "", fmt.Errorf("unknown relationshipType: '%s'", lastPart)
+}
diff --git a/rdfloader/parser2v2/parse_relationship_test.go b/rdfloader/parser2v2/parse_relationship_test.go
new file mode 100644
index 0000000..fd66d14
--- /dev/null
+++ b/rdfloader/parser2v2/parse_relationship_test.go
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func Test_getReferenceFromURI(t *testing.T) {
+ // TestCase 1: noassertion uri
+ ref, err := getReferenceFromURI(SPDX_NOASSERTION_CAPS)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "NOASSERTION" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "NOASSERTION")
+ }
+
+ // TestCase 2: NONE uri
+ ref, err = getReferenceFromURI(SPDX_NONE_CAPS)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "NONE" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "NONE")
+ }
+
+ // TestCase 3: Valid URI
+ ref, err = getReferenceFromURI(NS_SPDX + "SPDXRef-item1")
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "item1" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "item1")
+ }
+
+ // TestCase 3: Invalid URI
+ _, err = getReferenceFromURI(NS_SPDX + "item1")
+ if err == nil {
+ t.Errorf("should've raised an error for invalid input")
+ }
+}
+
+func Test_getRelationshipTypeFromURI(t *testing.T) {
+ // TestCase 1: valid relationshipType
+ relnType := "expandedFromArchive"
+ op, err := getRelationshipTypeFromURI(NS_SPDX + "relationshipType_" + relnType)
+ if err != nil {
+ t.Errorf("error getting relationship type from a valid input")
+ }
+ if op != relnType {
+ t.Errorf("expected %s, found %s", relnType, op)
+ }
+
+ // TestCase2: invalid relationshipType
+ relnType = "invalidRelationship"
+ _, err = getRelationshipTypeFromURI(NS_SPDX + "relationshipType_" + relnType)
+ if err == nil {
+ t.Errorf("should've raised an error for an invalid input(%s)", relnType)
+ }
+}
+
+func Test_rdfParser2_2_parseRelatedElementFromTriple(t *testing.T) {
+ // TestCase 1: Package as a related element
+ parser, _ := parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln := &v2_2.Relationship{}
+ triple := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)[0]
+ err := parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 3: invalid package as a relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_2.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Package id, got %v", err)
+ }
+
+ // TestCase 4: valid File as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_2.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 5: invalid File as a relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_2.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error while parsing an invalid File, got %v", err)
+ }
+
+ // TestCase 6: valid SpdxElement as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_2.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_SPDX_ELEMENT)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "File",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 7: invalid SpdxElement as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-:File"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_2.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_SPDX_ELEMENT)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid documentId for SpdxElement, got %v", err)
+ }
+}
+
+func Test_rdfParser2_2_parseRelationship(t *testing.T) {
+ // TestCase 1: invalid RefA
+ parser, _ := parserFromBodyContent(`
+ <spdx:File>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err := parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid RefA")
+ }
+
+ // TestCase 3: invalid RefB
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid RefB")
+ }
+
+ // TestCase 3: more than one typeTriple for relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <spdx:relatedSpdxElement>
+ <spdx:File/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to more than one type triples")
+ }
+
+ // TestCase 4: undefined relatedSpdxElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Unknown rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to unknown relatedElement, got %v", err)
+ }
+
+ // TestCase 6: relatedElement associated with more than one type
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid relatedElement, got %v", err)
+ }
+
+ // TestCase 5: unknown predicate inside a relationship
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Unknown rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <spdx:unknownPredicate/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to unknown predicate in a relationship")
+ }
+
+ // TestCase 8: Recursive relationships mustn't raise any error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship rdf:about="#SPDXRef-reln">
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon">
+ <spdx:relationship>
+ <spdx:Relationship rdf:about="#SPDXRef-reln">
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+
+ // TestCase 7: completely valid example:
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid relationship: %v", err)
+ }
+ // validating parsed attributes
+ if len(parser.doc.Relationships) != 1 {
+ t.Errorf("after parsing a valid relationship, doc should've had 1 relationship, found %d", len(parser.doc.Relationships))
+ }
+ reln := parser.doc.Relationships[0]
+ expectedRelnType := "describes"
+ if reln.Relationship != expectedRelnType {
+ t.Errorf("expected %s, found %s", expectedRelnType, reln.Relationship)
+ }
+ expectedRefA := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "File",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+ expectedComment := "comment"
+ if reln.RelationshipComment != expectedComment {
+ t.Errorf("expected %v, found %v", expectedComment, reln.RelationshipComment)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_review.go b/rdfloader/parser2v2/parse_review.go
new file mode 100644
index 0000000..40c87d3
--- /dev/null
+++ b/rdfloader/parser2v2/parse_review.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *rdfParser2_2) setReviewFromNode(reviewedNode *gordfParser.Node) error {
+ review := v2_2.Review{}
+ for _, triple := range parser.nodeToTriples(reviewedNode) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ review.ReviewComment = triple.Object.ID
+ case SPDX_REVIEW_DATE:
+ // cardinality: exactly 1
+ review.ReviewDate = triple.Object.ID
+ case SPDX_REVIEWER:
+ // cardinality: max 1
+ var err error
+ review.ReviewerType, review.Reviewer, err = ExtractSubs(triple.Object.ID, ":")
+ if err != nil {
+ return fmt.Errorf("error parsing reviewer: %v", err)
+ }
+ default:
+ return fmt.Errorf("unknown predicate %v for review triples", triple.Predicate)
+ }
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, &review)
+ return nil
+}
diff --git a/rdfloader/parser2v2/parse_review_test.go b/rdfloader/parser2v2/parse_review_test.go
new file mode 100644
index 0000000..d62b7b4
--- /dev/null
+++ b/rdfloader/parser2v2/parse_review_test.go
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "testing"
+)
+
+func Test_rdfParser2_2_setReviewFromNode(t *testing.T) {
+ // TestCase 1: unknown predicate must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ <spdx:unknown />
+ </spdx:Review>
+ `)
+ reviewNode := parser.gordfParserObj.Triples[0].Subject
+ err := parser.setReviewFromNode(reviewNode)
+ if err == nil {
+ t.Errorf("unknown predicate should've elicit an error")
+ }
+
+ // TestCase 2: wrong reviewer format must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ `)
+ reviewNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setReviewFromNode(reviewNode)
+ if err == nil {
+ t.Errorf("incorrect should've elicit an error")
+ }
+
+ // TestCase 3: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne</spdx:reviewer>
+ </spdx:Review>
+ `)
+ reviewNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setReviewFromNode(reviewNode)
+ if err != nil {
+ t.Errorf("error parsing a valid node")
+ }
+ n := len(parser.doc.Reviews)
+ if n != 1 {
+ t.Errorf("expected doc to have 1 review, found %d", n)
+ }
+ review := parser.doc.Reviews[0]
+ expectedComment := "Another example reviewer."
+ if review.ReviewComment != expectedComment {
+ t.Errorf("expected: %v, found: %s", expectedComment, review.ReviewComment)
+ }
+ expectedDate := "2011-03-13T00:00:00Z"
+ if review.ReviewDate != expectedDate {
+ t.Errorf("expected %s, found %s", expectedDate, review.ReviewDate)
+ }
+ expectedReviewer := "Suzanne"
+ if review.Reviewer != expectedReviewer {
+ t.Errorf("expected %s, found %s", expectedReviewer, review.Reviewer)
+ }
+ expectedReviewerType := "Person"
+ if review.ReviewerType != expectedReviewerType {
+ t.Errorf("expected %s, found %s", expectedReviewerType, review.ReviewerType)
+ }
+}
diff --git a/rdfloader/parser2v2/parse_snippet_info.go b/rdfloader/parser2v2/parse_snippet_info.go
new file mode 100644
index 0000000..5e80944
--- /dev/null
+++ b/rdfloader/parser2v2/parse_snippet_info.go
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strconv"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// Snippet Information
+// Cardinality: Optional, Many
+func (parser *rdfParser2_2) getSnippetInformationFromNode2_2(node *gordfParser.Node) (si *v2_2.Snippet, err error) {
+ si = &v2_2.Snippet{}
+
+ err = setSnippetID(node.ID, si)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, siTriple := range parser.nodeToTriples(node) {
+ switch siTriple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ case SPDX_SNIPPET_FROM_FILE:
+ // cardinality: exactly 1
+ // file which is associated with the snippet
+ _, err := parser.getFileFromNode(siTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ docElemID, err := ExtractDocElementID(getLastPartOfURI(siTriple.Object.ID))
+ si.SnippetFromFileSPDXIdentifier = docElemID.ElementRefID
+ case SPDX_RANGE:
+ // cardinality: min 1
+ err = parser.setSnippetRangeFromNode(siTriple.Object, si)
+ if err != nil {
+ return nil, err
+ }
+ case SPDX_LICENSE_INFO_IN_SNIPPET:
+ // license info in snippet can be NONE, NOASSERTION or SimpleLicensingInfo
+ // using AnyLicenseInfo because it can redirect the request and
+ // can handle NONE & NOASSERTION
+ var anyLicense AnyLicenseInfo
+ anyLicense, err = parser.getAnyLicenseFromNode(siTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license info in snippet: %v", err)
+ }
+ si.LicenseInfoInSnippet = append(si.LicenseInfoInSnippet, anyLicense.ToLicenseString())
+ case SPDX_NAME:
+ si.SnippetName = siTriple.Object.ID
+ case SPDX_COPYRIGHT_TEXT:
+ si.SnippetCopyrightText = siTriple.Object.ID
+ case SPDX_LICENSE_COMMENTS:
+ si.SnippetLicenseComments = siTriple.Object.ID
+ case RDFS_COMMENT:
+ si.SnippetComment = siTriple.Object.ID
+ case SPDX_LICENSE_CONCLUDED:
+ var anyLicense AnyLicenseInfo
+ anyLicense, err = parser.getAnyLicenseFromNode(siTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license info in snippet: %v", err)
+ }
+ si.SnippetLicenseConcluded = anyLicense.ToLicenseString()
+ default:
+ return nil, fmt.Errorf("unknown predicate %v", siTriple.Predicate.ID)
+ }
+ }
+ return si, nil
+}
+
+// given is the id of the file, sets the snippet to the file in parser.
+func (parser *rdfParser2_2) setSnippetToFileWithID(snippet *v2_2.Snippet, fileID common.ElementID) error {
+ if parser.files[fileID] == nil {
+ return fmt.Errorf("snippet refers to an undefined file with ID: %s", fileID)
+ }
+
+ // initializing snippet of the files if it is not defined already
+ if parser.files[fileID].Snippets == nil {
+ parser.files[fileID].Snippets = map[common.ElementID]*v2_2.Snippet{}
+ }
+
+ // setting the snippet to the file.
+ parser.files[fileID].Snippets[snippet.SnippetSPDXIdentifier] = snippet
+
+ return nil
+}
+
+func (parser *rdfParser2_2) setSnippetRangeFromNode(node *gordfParser.Node, si *v2_2.Snippet) error {
+ // for a range object, we can have only 3 associated triples:
+ // node -> RDF_TYPE -> Object
+ // node -> startPointer -> Object
+ // node -> endPointer -> Object
+ associatedTriples := parser.nodeToTriples(node)
+ if len(associatedTriples) != 3 {
+ return fmt.Errorf("range should be associated with exactly 3 triples, got %d", len(associatedTriples))
+ }
+
+ // Triple 1: Predicate=RDF_TYPE
+ typeTriple := rdfwriter.FilterTriples(associatedTriples, &node.ID, &RDF_TYPE, nil)
+ if len(typeTriple) != 1 {
+ // we had 3 associated triples. out of which 2 is start and end pointer,
+ // if we do not have the rdf:type triple as the third one,
+ // we have either extra or undefined predicate.
+ return fmt.Errorf("every object node must be associated with exactly one rdf:type triple, found: %d", len(typeTriple))
+ }
+
+ // getting start pointer
+ startPointerTriples := rdfwriter.FilterTriples(associatedTriples, &node.ID, &PTR_START_POINTER, nil)
+ if len(startPointerTriples) != 1 {
+ return fmt.Errorf("range object must be associated with exactly 1 startPointer, got %d", len(startPointerTriples))
+ }
+ startRangeType, start, err := parser.getPointerFromNode(startPointerTriples[0].Object, si)
+ if err != nil {
+ return fmt.Errorf("error parsing startPointer: %v", err)
+ }
+
+ // getting end pointer
+ endPointerTriples := rdfwriter.FilterTriples(associatedTriples, &node.ID, &PTR_END_POINTER, nil)
+ if len(startPointerTriples) != 1 {
+ return fmt.Errorf("range object must be associated with exactly 1 endPointer, got %d", len(endPointerTriples))
+ }
+ endRangeType, end, err := parser.getPointerFromNode(endPointerTriples[0].Object, si)
+ if err != nil {
+ return fmt.Errorf("error parsing endPointer: %v", err)
+ }
+
+ // return error when start and end pointer type is not same.
+ if startRangeType != endRangeType {
+ return fmt.Errorf("start and end range type doesn't match")
+ }
+
+ si.Ranges = []common.SnippetRange{{
+ StartPointer: common.SnippetRangePointer{FileSPDXIdentifier: si.SnippetFromFileSPDXIdentifier},
+ EndPointer: common.SnippetRangePointer{FileSPDXIdentifier: si.SnippetFromFileSPDXIdentifier},
+ }}
+
+ if startRangeType == LINE_RANGE {
+ si.Ranges[0].StartPointer.LineNumber = start
+ si.Ranges[0].EndPointer.LineNumber = end
+ } else {
+ si.Ranges[0].StartPointer.Offset = start
+ si.Ranges[0].EndPointer.Offset = end
+ }
+ return nil
+}
+
+func (parser *rdfParser2_2) getPointerFromNode(node *gordfParser.Node, si *v2_2.Snippet) (rt RangeType, number int, err error) {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ case PTR_REFERENCE:
+ err = parser.parseRangeReference(triple.Object, si)
+ case PTR_OFFSET:
+ number, err = strconv.Atoi(triple.Object.ID)
+ rt = BYTE_RANGE
+ case PTR_LINE_NUMBER:
+ number, err = strconv.Atoi(triple.Object.ID)
+ rt = LINE_RANGE
+ default:
+ err = fmt.Errorf("undefined predicate (%s) for a pointer", triple.Predicate)
+ }
+ if err != nil {
+ return
+ }
+ }
+ if rt == "" {
+ err = fmt.Errorf("range type not defined for a pointer")
+ }
+ return
+}
+
+func (parser *rdfParser2_2) parseRangeReference(node *gordfParser.Node, snippet *v2_2.Snippet) error {
+ // reference is supposed to be either a resource reference to an already
+ // defined or a new file. Unfortunately, I didn't find field where this can be set in the tools-golang data model.
+ // todo: set this reference to the snippet
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ if len(associatedTriples) == 0 {
+ return nil
+ }
+ _, err := parser.getFileFromNode(node)
+ if err != nil {
+ return fmt.Errorf("error parsing a new file in a reference: %v", err)
+ }
+ return nil
+}
+
+func setSnippetID(uri string, si *v2_2.Snippet) (err error) {
+ fragment := getLastPartOfURI(uri)
+ si.SnippetSPDXIdentifier, err = ExtractElementID(fragment)
+ if err != nil {
+ return fmt.Errorf("error setting snippet identifier: %v", uri)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v2/parse_snippet_info_test.go b/rdfloader/parser2v2/parse_snippet_info_test.go
new file mode 100644
index 0000000..df0bb70
--- /dev/null
+++ b/rdfloader/parser2v2/parse_snippet_info_test.go
@@ -0,0 +1,536 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func Test_rdfParser2_2_getSnippetInformationFromTriple2_2(t *testing.T) {
+ var err error
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid snippet id:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#Snippet">
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid, got %v", err)
+ }
+
+ // TestCase 2: Invalid LicenseInfoInSnippet
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/licenses/Unknown"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid licenseInfoInSnippet, got %v", err)
+ }
+
+ // TestCase 3: Invalid range.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:range>
+ <spdx:StartEndPointer>
+ <spdx:unknownTag />
+ </spdx:StartEndPointer>
+ </spdx:range>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid range, got %v", err)
+ }
+
+ // TestCase 3: invalid file in snippetFromFile
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:snippetFromFile>
+ <spdx:File rdf:resource="http://anupam-VirtualBox/spdx.rdf#item8" />
+ </spdx:snippetFromFile>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid snippetFromFile, got %v", err)
+ }
+
+ // TestCase 4: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:unknownPredicate />
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, got %v", err)
+ }
+
+ // TestCase 5: invalid license concluded:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/Unknown"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid licenseConcluded, got %v", err)
+ }
+
+ // TestCase 6: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:snippetFromFile>
+ <spdx:File rdf:about="#SPDXRef-File" />
+ </spdx:snippetFromFile>
+ <spdx:range>
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>420</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ </spdx:range>
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ <spdx:name>snippet test</spdx:name>
+ <spdx:copyrightText>test</spdx:copyrightText>
+ <spdx:licenseComments>comments</spdx:licenseComments>
+ <rdfs:comment>comments</rdfs:comment>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_2(node)
+ if err != nil {
+ t.Fatalf("error parsing a valid example: %v", err)
+ }
+}
+
+func Test_setSnippetID(t *testing.T) {
+ // TestCase 1: invalid input (empty)
+ err := setSnippetID("", &v2_2.Snippet{})
+ if err == nil {
+ t.Errorf("should've raised an error for empty input")
+ }
+
+ // TestCase 2: valid input
+ si := &v2_2.Snippet{}
+ err = setSnippetID("http://spdx.org/spdxdocs/spdx-example#SPDXRef-Snippet", si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if si.SnippetSPDXIdentifier != "Snippet" {
+ t.Errorf("expected: %s, found: %s", "Snippet", si.SnippetSPDXIdentifier)
+ }
+}
+
+func Test_rdfParser2_2_parseRangeReference(t *testing.T) {
+ var err error
+ var node *gordfParser.Node
+ var parser *rdfParser2_2
+ var si *v2_2.Snippet
+
+ // TestCase 1: ResourceLiteral node without a new file shouldn't raise any error.
+ si = &v2_2.Snippet{}
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.RESOURCELITERAL,
+ ID: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource",
+ }
+ err = parser.parseRangeReference(node, si)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+
+ // TestCase 2: invalid file in the reference should raise an error
+ si = &v2_2.Snippet{}
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#DoapSource">
+ <spdx:fileName> test file </spdx:fileName>
+ </spdx:File>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseRangeReference(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid file in the range reference, got %v", err)
+ }
+
+ // TestCase 3: A valid reference must set the file to the files map of the parser.
+ si = &v2_2.Snippet{}
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource">
+ <spdx:fileName> test file </spdx:fileName>
+ </spdx:File>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseRangeReference(node, si)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ if len(parser.files) != 1 {
+ t.Errorf("expected parser.files to have 1 file, found %d", len(parser.files))
+ }
+}
+
+func Test_rdfParser2_2_getPointerFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var si *v2_2.Snippet
+ var err error
+ var rt RangeType
+ var number int
+
+ // TestCase 1: invalid number in the offset field must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>3-10</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid offset, got %v", err)
+ }
+
+ // TestCase 2: invalid number in the lineNumber field must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>3-10</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid offset, got %v", err)
+ }
+
+ // TestCase 3: invalid predicate in the pointer field
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <spdx:invalidTag />
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>3-10</j.0:lineNumber>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid predicate, got %v", err)
+ }
+
+ // TestCase 4: No range type defined must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid rangeType, got %v", err)
+ }
+
+ // TestCase 5: valid example
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ rt, number, err = parser.getPointerFromNode(node, si)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid node: %v", err)
+ }
+ if rt != BYTE_RANGE {
+ t.Errorf("expected: %s, got: %s", BYTE_RANGE, rt)
+ }
+ if number != 310 {
+ t.Errorf("expected: %d, got: %d", 310, number)
+ }
+}
+
+func Test_rdfParser2_2_setSnippetRangeFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+ var si *v2_2.Snippet
+ var node *gordfParser.Node
+
+ // TestCase 1: range with less one pointer less must raise an error
+ // (end-pointer missing in the range)
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing end pointer, got %v", err)
+ }
+
+ // TestCase 2: triples with 0 or more than one type-triple
+ parser, _ = parserFromBodyContent(`
+
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ dummyTriple := parser.gordfParserObj.Triples[0]
+ // resetting the node to be associated with 3 triples which will have
+ // rdf:type triple either thrice or 0 times.
+ parser.nodeStringToTriples[node.String()] = []*gordfParser.Triple{
+ dummyTriple, dummyTriple, dummyTriple,
+ }
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid rdf:type triples, got %v", err)
+ }
+
+ // TestCase 3: triples with 0 startPointer
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing start pointer, got %v", err)
+ }
+
+ // TestCase 4: triples with 0 endPointer
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing end pointer, got %v", err)
+ }
+
+ // TestCase 5: error parsing start pointer must be propagated to the range
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>42.0</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid start pointer, got %v", err)
+ }
+
+ // TestCase 6: error parsing end pointer must be propagated to the range
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>31+0</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid end pointer, got %v", err)
+ }
+
+ // TestCase 7: mismatching start and end pointer must also raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to mismatching start and end pointers, got %v", err)
+ }
+
+ // TestCase 8: everything valid(byte_range):
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ // TestCase 9: everything valid(line_range):
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>420</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_2.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_setSnippetToFileWithID(t *testing.T) {
+ var parser *rdfParser2_2
+ var fileId common.ElementID
+ var si *v2_2.Snippet
+ var file *v2_2.File
+ var err error
+
+ // TestCase 1: file id which is not associated with any file must raise an error.
+ parser, _ = parserFromBodyContent("")
+ si = &v2_2.Snippet{}
+ err = parser.setSnippetToFileWithID(si, fileId)
+ if err == nil {
+ t.Errorf("expected an error saying undefined file")
+ }
+
+ // TestCase 2: file exists, but snippet of the file doesn't ( it mustn't raise any error )
+ fileId = common.ElementID("File1")
+ file = &v2_2.File{
+ FileSPDXIdentifier: fileId,
+ }
+ parser.files[fileId] = file
+ file.Snippets = nil // nil snippets
+ err = parser.setSnippetToFileWithID(si, fileId)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if len(file.Snippets) != 1 {
+ t.Errorf("expected file to have 1 snippet, got %d", len(file.Snippets))
+ }
+}
diff --git a/rdfloader/parser2v2/parse_spdx_document.go b/rdfloader/parser2v2/parse_spdx_document.go
new file mode 100644
index 0000000..481e8cc
--- /dev/null
+++ b/rdfloader/parser2v2/parse_spdx_document.go
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *rdfParser2_2) parseSpdxDocumentNode(spdxDocNode *gordfParser.Node) (err error) {
+ // shorthand for document's creation info.
+ ci := parser.doc.CreationInfo
+
+ // parse the document header information (SPDXID and document namespace)
+ // the Subject.ID is of type baseURI#spdxID
+ baseUri, offset, err := ExtractSubs(spdxDocNode.ID, "#")
+ if err != nil {
+ return err
+ }
+ parser.doc.DocumentNamespace = baseUri // 2.5
+ parser.doc.SPDXIdentifier = common.ElementID(offset) // 2.3
+
+ // parse other associated triples.
+ for _, subTriple := range parser.nodeToTriples(spdxDocNode) {
+ objectValue := subTriple.Object.ID
+ switch subTriple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_SPEC_VERSION: // 2.1: specVersion
+ // cardinality: exactly 1
+ parser.doc.SPDXVersion = objectValue
+ case SPDX_DATA_LICENSE: // 2.2: dataLicense
+ // cardinality: exactly 1
+ dataLicense, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.DataLicense = dataLicense.ToLicenseString()
+ case SPDX_NAME: // 2.4: DocumentName
+ // cardinality: exactly 1
+ parser.doc.DocumentName = objectValue
+ case SPDX_EXTERNAL_DOCUMENT_REF: // 2.6: externalDocumentReferences
+ // cardinality: min 0
+ var extRef v2_2.ExternalDocumentRef
+ extRef, err = parser.getExternalDocumentRefFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.ExternalDocumentReferences = append(parser.doc.ExternalDocumentReferences, extRef)
+ case SPDX_CREATION_INFO: // 2.7 - 2.10:
+ // cardinality: exactly 1
+ err = parser.parseCreationInfoFromNode(ci, subTriple.Object)
+ case RDFS_COMMENT: // 2.11: Document Comment
+ // cardinality: max 1
+ parser.doc.DocumentComment = objectValue
+ case SPDX_REVIEWED: // reviewed:
+ // cardinality: min 0
+ err = parser.setReviewFromNode(subTriple.Object)
+ case SPDX_DESCRIBES_PACKAGE: // describes Package
+ // cardinality: min 0
+ var pkg *v2_2.Package
+ pkg, err = parser.getPackageFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.Packages = append(parser.doc.Packages, pkg)
+ case SPDX_HAS_EXTRACTED_LICENSING_INFO: // hasExtractedLicensingInfo
+ // cardinality: min 0
+ extractedLicensingInfo, err := parser.getExtractedLicensingInfoFromNode(subTriple.Object)
+ if err != nil {
+ return fmt.Errorf("error setting extractedLicensingInfo in spdxDocument: %v", err)
+ }
+ othLicense := parser.extractedLicenseToOtherLicense(extractedLicensingInfo)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, &othLicense)
+ case SPDX_RELATIONSHIP: // relationship
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ case SPDX_ANNOTATION: // annotations
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ default:
+ return fmt.Errorf("invalid predicate while parsing SpdxDocument: %v", subTriple.Predicate)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (parser *rdfParser2_2) getExternalDocumentRefFromNode(node *gordfParser.Node) (edr v2_2.ExternalDocumentRef, err error) {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_EXTERNAL_DOCUMENT_ID:
+ // cardinality: exactly 1
+ edr.DocumentRefID = triple.Object.ID
+ case SPDX_SPDX_DOCUMENT:
+ // cardinality: exactly 1
+ // assumption: "spdxDocument" property of an external document
+ // reference is just a uri which doesn't follow a spdxDocument definition
+ edr.URI = triple.Object.ID
+ case SPDX_CHECKSUM:
+ // cardinality: exactly 1
+ alg, checksum, err := parser.getChecksumFromNode(triple.Object)
+ if err != nil {
+ return edr, err
+ }
+ edr.Checksum.Value = checksum
+ edr.Checksum.Algorithm = alg
+ case RDF_TYPE:
+ continue
+ default:
+ return edr, fmt.Errorf("unknown predicate ID (%s) while parsing externalDocumentReference", triple.Predicate.ID)
+ }
+ }
+ return edr, nil
+}
diff --git a/rdfloader/parser2v2/parse_spdx_document_test.go b/rdfloader/parser2v2/parse_spdx_document_test.go
new file mode 100644
index 0000000..9d22faa
--- /dev/null
+++ b/rdfloader/parser2v2/parse_spdx_document_test.go
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_2_getExternalDocumentRefFromNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid checksum, found %v", err)
+ }
+
+ // TestCase 2: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:unknownTag />
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, found %v", err)
+ }
+
+ // TestCase 3: valid example
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_parseSpdxDocumentNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid spdx id of the document
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301/Document"/>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid document id, got %v", err)
+ }
+
+ // TestCase 2: erroneous dataLicense
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:dataLicense rdf:resource="http://spdx.org/rdf/terms#Unknown" />
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid dataLicense, got %v", err)
+ }
+
+ // TestCase 3: invalid external document ref
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid externalDocumentRef, got %v", err)
+ }
+
+ // TestCase 4: invalid package
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:describesPackage>
+ <spdx:Package rdf:about="http://www.spdx.org/spdxdocs/8f141b09-1138-4fc5-aecb-fc10d9ac1eed#SPDX-1"/>
+ </spdx:describesPackage>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid externalDocumentRef, got %v", err)
+ }
+
+ // TestCase 5: error in extractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:hasExtractedLicensingInfo>
+ <spdx:ExtractedLicensingInfo rdf:about="#LicenseRef-Freeware">
+ <spdx:invalidTag />
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText>
+<![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]>
+ </spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:hasExtractedLicensingInfo>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid extractedLicensingInfo, got %v", err)
+ }
+
+ // TestCase 6: error in annotation
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid extractedLicensingInfo, got %v", err)
+ }
+
+ // TestCase 7: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:unknownTag />
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to unknown predicate, got %v", err)
+ }
+
+ // TestCase 7: everything valid
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:specVersion>SPDX-2.1</spdx:specVersion>
+ <spdx:dataLicense rdf:resource="http://spdx.org/licenses/CC0-1.0" />
+ <spdx:name>/test/example</spdx:name>
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ <spdx:creationInfo>
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: spdx (y)</spdx:creator>
+ <spdx:creator>Organization: </spdx:creator>
+ <spdx:creator>Tool: spdx2</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ </spdx:CreationInfo>
+ </spdx:creationInfo>
+ <rdfs:comment>test</rdfs:comment>
+ <spdx:reviewed>
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ </spdx:reviewed>
+ <spdx:describesPackage>
+ <spdx:Package rdf:about="#SPDXRef-1"/>
+ </spdx:describesPackage>
+ <spdx:hasExtractedLicensingInfo rdf:resource="http://spdx.org/licenses/CC0-1.0"/>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_containedBy"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-1"/>
+ <rdfs:comment></rdfs:comment>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>test annotation</rdfs:comment>
+ <spdx:annotator>Person: Rishabh Bhatnagar</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v2/parser.go b/rdfloader/parser2v2/parser.go
new file mode 100644
index 0000000..4b3b62c
--- /dev/null
+++ b/rdfloader/parser2v2/parser.go
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "errors"
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ gordfWriter "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// returns a new instance of rdfParser2_2 given the gordf object and nodeToTriples mapping
+func NewParser2_2(gordfParserObj *gordfParser.Parser, nodeToTriples map[string][]*gordfParser.Triple) *rdfParser2_2 {
+ parser := rdfParser2_2{
+ gordfParserObj: gordfParserObj,
+ nodeStringToTriples: nodeToTriples,
+ doc: &v2_2.Document{
+ ExternalDocumentReferences: []v2_2.ExternalDocumentRef{},
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{},
+ Files: []*v2_2.File{},
+ OtherLicenses: []*v2_2.OtherLicense{},
+ Relationships: []*v2_2.Relationship{},
+ Annotations: []*v2_2.Annotation{},
+ Reviews: []*v2_2.Review{},
+ },
+ files: map[common.ElementID]*v2_2.File{},
+ assocWithPackage: map[common.ElementID]bool{},
+ cache: map[string]*nodeState{},
+ }
+ return &parser
+}
+
+// main function which takes in a gordfParser and returns
+// a spdxDocument model or the error encountered while parsing it
+func LoadFromGoRDFParser(gordfParserObj *gordfParser.Parser) (*v2_2.Document, error) {
+ // nodeToTriples is a mapping from a node to list of triples.
+ // for every node in the set of subjects of all the triples,
+ // it provides a list of triples that are associated with that subject node.
+ nodeToTriples := gordfWriter.GetNodeToTriples(gordfParserObj.Triples)
+ parser := NewParser2_2(gordfParserObj, nodeToTriples)
+
+ spdxDocumentNode, err := parser.getSpdxDocNode()
+ if err != nil {
+ return nil, err
+ }
+
+ err = parser.parseSpdxDocumentNode(spdxDocumentNode)
+ if err != nil {
+ return nil, err
+ }
+
+ // parsing other root elements
+ for _, rootNode := range gordfWriter.GetRootNodes(parser.gordfParserObj.Triples) {
+ typeTriples := gordfWriter.FilterTriples(gordfParserObj.Triples, &rootNode.ID, &RDF_TYPE, nil)
+ if len(typeTriples) != 1 {
+ return nil, fmt.Errorf("every node must be associated with exactly 1 type Triple. found %d type triples", len(typeTriples))
+ }
+ switch typeTriples[0].Object.ID {
+ case SPDX_SPDX_DOCUMENT_CAPITALIZED:
+ continue // it is already parsed.
+ case SPDX_SNIPPET:
+ snippet, err := parser.getSnippetInformationFromNode2_2(typeTriples[0].Subject)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing a snippet: %v", err)
+ }
+ err = parser.setSnippetToFileWithID(snippet, snippet.SnippetFromFileSPDXIdentifier)
+ if err != nil {
+ return nil, err
+ }
+ // todo: check other root node attributes.
+ default:
+ continue
+ // because in rdf it is quite possible that the root node is an
+ // element that has been used in the some other element as a child
+ }
+ }
+
+ // parsing packages and files sets the files to a files variable which is
+ // associated with the parser and not the document. following method is
+ // necessary to transfer the files which are not set in the packages to the
+ // Files attribute of the document
+ // WARNING: do not relocate following function call. It must be at the end of the function
+ parser.setUnpackagedFiles()
+ return parser.doc, nil
+}
+
+// from the given parser object, returns the SpdxDocument Node defined in the root elements.
+// returns error if the document is associated with no SpdxDocument or
+// associated with more than one SpdxDocument node.
+func (parser *rdfParser2_2) getSpdxDocNode() (node *gordfParser.Node, err error) {
+ /* Possible Questions:
+ 1. why are you traversing the root nodes only? why not directly filter out
+ all the triples with rdf:type=spdx:SpdxDocument?
+ Ans: It is quite possible that the relatedElement or any other attribute
+ to have dependency of another SpdxDocument. In that case, that
+ element will reference the dependency using SpdxDocument tag which will
+ cause false positives when direct filtering is done.
+ */
+ // iterate over root nodes and find the node which has a property of rdf:type=spdx:SpdxDocument
+ var spdxDocNode *gordfParser.Node
+ for _, rootNode := range gordfWriter.GetRootNodes(parser.gordfParserObj.Triples) {
+ typeTriples := gordfWriter.FilterTriples(
+ parser.nodeToTriples(rootNode), // triples
+ &rootNode.ID, // Subject
+ &RDF_TYPE, // Predicate
+ nil, // Object
+ )
+
+ if typeTriples[0].Object.ID == SPDX_SPDX_DOCUMENT_CAPITALIZED {
+ // we found a SpdxDocument Node
+
+ // must be associated with exactly one rdf:type.
+ if len(typeTriples) != 1 {
+ return nil, fmt.Errorf("rootNode (%v) must be associated with exactly one"+
+ " triple of predicate rdf:type, found %d triples", rootNode, len(typeTriples))
+ }
+
+ // checking if we've already found a node and it is not same as the current one.
+ if spdxDocNode != nil && spdxDocNode.ID != typeTriples[0].Subject.ID {
+ return nil, fmt.Errorf("found more than one SpdxDocument Node (%v and %v)", spdxDocNode, typeTriples[0].Subject)
+ }
+ spdxDocNode = typeTriples[0].Subject
+ }
+ }
+ if spdxDocNode == nil {
+ return nil, errors.New("RDF files must be associated with a SpdxDocument tag. No tag found")
+ }
+ return spdxDocNode, nil
+}
diff --git a/rdfloader/parser2v2/parser_test.go b/rdfloader/parser2v2/parser_test.go
new file mode 100644
index 0000000..11b2da6
--- /dev/null
+++ b/rdfloader/parser2v2/parser_test.go
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "testing"
+)
+
+func TestNewParser2_2(t *testing.T) {
+ // testing if the attributes are initialised well and no top-level is left uninitialized.
+ // primarily, checking if all the maps are initialized because
+ // uninitialized slices are by default slices of length 0
+ p, _ := parserFromBodyContent(``)
+ parser := NewParser2_2(p.gordfParserObj, p.nodeStringToTriples)
+ if parser.files == nil {
+ t.Errorf("files should've been initialised, got %v", parser.files)
+ }
+ if parser.assocWithPackage == nil {
+ t.Errorf("assocWithPackage should've been initialised, got %v", parser.assocWithPackage)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo should've been initialised, got %v", parser.doc.CreationInfo)
+ }
+ if parser.doc.Packages == nil {
+ t.Errorf("doc.Packages should've been initialised, got %v", parser.doc.Packages)
+ }
+ if parser.doc.Files == nil {
+ t.Errorf("doc.Files should've been initialised, got %v", parser.doc.Files)
+ }
+}
+
+func TestLoadFromGoRDFParser(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+
+ // TestCase 1: gordfparser without a SpdxDocument node triple:
+ parser, _ = parserFromBodyContent("")
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error because of absence of SpdxDocument node, got %v", err)
+ }
+
+ // TestCase 2: invalid SpdxDocumentNode
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301/Document">
+ <spdx:invalidTag />
+ </spdx:SpdxDocument>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error because of absence of SpdxDocument node, got %v", err)
+ }
+
+ // TestCase 3: >1 type triples for subnode of a SpdxDocument:
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#Snippet"/>
+ <spdx:CreationInfo rdf:about="#Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to more than one type triples, got %v", err)
+ }
+
+ // TestCase 4: invalid snippet must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Snippet, got %v", err)
+ }
+
+ // TestCase 5: invalid snippet not associated with any File must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Snippet File, got %v", err)
+ }
+
+ // TestCase 6: other Tag alongwith the SpdxDocument node mustn't raise any error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:review/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ // TestCase 5: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:name>from linux kernel</spdx:name>
+ <spdx:copyrightText>Copyright 2008-2010 John Smith</spdx:copyrightText>
+ <spdx:licenseComments>The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.</spdx:licenseComments>
+ <spdx:snippetFromFile>
+ <spdx:File rdf:about="#SPDXRef-DoapSource">
+ <spdx:copyrightText>Copyright 2010, 2011 Source Auditor Inc.</spdx:copyrightText>
+ <spdx:fileContributor>Open Logic Inc.</spdx:fileContributor>
+ <spdx:fileName>./src/org/spdx/parser/DOAPProject.java</spdx:fileName>
+ <spdx:fileContributor>Black Duck Software In.c</spdx:fileContributor>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/Apache-2.0"/>
+ </spdx:File>
+ </spdx:snippetFromFile>
+ </spdx:Snippet>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err != nil {
+ t.Errorf("error parsing a valid example: %v", err)
+ }
+}
+
+func Test_rdfParser2_2_getSpdxDocNode(t *testing.T) {
+ var parser *rdfParser2_2
+ var err error
+
+ // TestCase 1: more than one association type for a single node.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Document"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ t.Log(err)
+ if err == nil {
+ t.Errorf("expected and error due to more than one type triples for the SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 2: must be associated with exactly one rdf:type.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Document"/>
+ <spdx:File rdf:about="#SPDXRef-DoapSource"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ t.Log(err)
+ if err == nil {
+ t.Errorf("rootNode must be associated with exactly one triple of predicate rdf:type, got %v", err)
+ }
+
+ // TestCase 3: two different spdx nodes found in a single document.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-1"/>
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-2"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ if err == nil {
+ t.Errorf("expected and error due to more than one type SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 4: no spdx document
+ parser, _ = parserFromBodyContent(``)
+ _, err = parser.getSpdxDocNode()
+ if err == nil {
+ t.Errorf("expected and error due to no SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 5: valid spdxDocument node
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-1"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v2/types.go b/rdfloader/parser2v2/types.go
new file mode 100644
index 0000000..dbb50d5
--- /dev/null
+++ b/rdfloader/parser2v2/types.go
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+// copied from tvloader/parser2v2/types.go
+package parser2v2
+
+import (
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+type rdfParser2_2 struct {
+ // fields associated with gordf project which
+ // will be required by rdfloader
+ gordfParserObj *gordfParser.Parser
+ nodeStringToTriples map[string][]*gordfParser.Triple
+
+ // document into which data is being parsed
+ doc *v2_2.Document
+
+ // map of packages and files.
+ files map[common.ElementID]*v2_2.File
+ assocWithPackage map[common.ElementID]bool
+
+ // mapping of nodeStrings to parsed object to save double computation.
+ cache map[string]*nodeState
+}
+
+type Color int
+
+const (
+ GREY Color = iota // represents that the node is being visited
+ WHITE // unvisited node
+ BLACK // visited node
+)
+
+type nodeState struct {
+ // object will be pointer to the parsed or element being parsed.
+ object interface{}
+ // color of a state represents if the node is visited/unvisited/being-visited.
+ Color Color
+}
+
+type AnyLicenseInfo interface {
+ // ToLicenseString returns the representation of license about how it will
+ // be stored in the tools-golang data model
+ ToLicenseString() string
+}
+
+type SimpleLicensingInfo struct {
+ AnyLicenseInfo
+ comment string
+ licenseID string
+ name string
+ seeAlso []string
+ example string
+}
+
+type ExtractedLicensingInfo struct {
+ SimpleLicensingInfo
+ extractedText string
+}
+
+type OrLaterOperator struct {
+ AnyLicenseInfo
+ member SimpleLicensingInfo
+}
+
+type ConjunctiveLicenseSet struct {
+ AnyLicenseInfo
+ members []AnyLicenseInfo
+}
+
+type DisjunctiveLicenseSet struct {
+ AnyLicenseInfo
+ members []AnyLicenseInfo
+}
+
+type License struct {
+ SimpleLicensingInfo
+ isOsiApproved bool
+ licenseText string
+ standardLicenseHeader string
+ standardLicenseTemplate string
+ standardLicenseHeaderTemplate string
+ isDeprecatedLicenseID bool
+ isFsfLibre bool
+}
+
+type ListedLicense struct {
+ License
+}
+
+type LicenseException struct {
+ licenseExceptionId string
+ licenseExceptionText string
+ seeAlso string // must be a valid uri
+ name string
+ example string
+ comment string
+}
+
+type WithExceptionOperator struct {
+ AnyLicenseInfo
+ member SimpleLicensingInfo
+ licenseException LicenseException
+}
+
+// custom LicenseType to provide support for licences of
+// type Noassertion, None and customLicenses
+type SpecialLicense struct {
+ AnyLicenseInfo
+ value SpecialLicenseValue
+}
+
+type SpecialLicenseValue string
+
+const (
+ NONE SpecialLicenseValue = "NONE"
+ NOASSERTION SpecialLicenseValue = "NOASSERTION"
+)
+
+type RangeType string
+
+const (
+ BYTE_RANGE RangeType = "byteRange"
+ LINE_RANGE RangeType = "lineRange"
+)
diff --git a/rdfloader/parser2v2/utils.go b/rdfloader/parser2v2/utils.go
new file mode 100644
index 0000000..9f51a3c
--- /dev/null
+++ b/rdfloader/parser2v2/utils.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ urilib "github.com/spdx/gordf/uri"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// a uri is of type baseURI#fragment or baseFragment/subFragment
+// returns fragment or subFragment when given as an input.
+func getLastPartOfURI(uri string) string {
+ if strings.Contains(uri, "#") {
+ parts := strings.Split(uri, "#")
+ return parts[len(parts)-1]
+ }
+ parts := strings.Split(uri, "/")
+ return parts[len(parts)-1]
+}
+
+func isUriValid(uri string) bool {
+ _, err := urilib.NewURIRef(uri)
+ return err == nil
+}
+
+func getNodeTypeFromTriples(triples []*gordfParser.Triple, node *gordfParser.Node) (string, error) {
+ if node == nil {
+ return "", errors.New("empty node passed to find node type")
+ }
+ typeTriples := rdfwriter.FilterTriples(triples, &node.ID, &RDF_TYPE, nil)
+ switch len(typeTriples) {
+ case 0:
+ return "", fmt.Errorf("node{%v} not associated with any type triple", node)
+ case 1:
+ return typeTriples[0].Object.ID, nil
+ default:
+ return "", fmt.Errorf("node{%v} is associated with more than one type triples", node)
+ }
+}
+
+func (parser *rdfParser2_2) nodeToTriples(node *gordfParser.Node) []*gordfParser.Triple {
+ if node == nil {
+ return []*gordfParser.Triple{}
+ }
+ return parser.nodeStringToTriples[node.String()]
+}
+
+// returns which boolean was given as an input
+// string(bool) is the only possible input for which it will not raise any error.
+func boolFromString(boolString string) (bool, error) {
+ switch strings.ToLower(boolString) {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ default:
+ return false, fmt.Errorf("boolean string can be either true/false")
+ }
+}
+
+/* Function Below this line is taken from the tvloader/parser2v2/utils.go */
+
+// used to extract DocumentRef and SPDXRef values from an SPDX Identifier
+// which can point either to this document or to a different one
+func ExtractDocElementID(value string) (common.DocElementID, error) {
+ docRefID := ""
+ idStr := value
+
+ // check prefix to see if it's a DocumentRef ID
+ if strings.HasPrefix(idStr, "DocumentRef-") {
+ // extract the part that comes between "DocumentRef-" and ":"
+ strs := strings.Split(idStr, ":")
+ // should be exactly two, part before and part after
+ if len(strs) < 2 {
+ return common.DocElementID{}, fmt.Errorf("no colon found although DocumentRef- prefix present")
+ }
+ if len(strs) > 2 {
+ return common.DocElementID{}, fmt.Errorf("more than one colon found")
+ }
+
+ // trim the prefix and confirm non-empty
+ docRefID = strings.TrimPrefix(strs[0], "DocumentRef-")
+ if docRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("document identifier has nothing after prefix")
+ }
+ // and use remainder for element ID parsing
+ idStr = strs[1]
+ }
+
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(idStr, "SPDXRef-") {
+ return common.DocElementID{}, fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(idStr, ":") {
+ // we know this means there was no DocumentRef- prefix, because
+ // we would have handled multiple colons above if it was
+ return common.DocElementID{}, fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(idStr, "SPDXRef-")
+ if eltRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.DocElementID{DocumentRefID: docRefID, ElementRefID: common.ElementID(eltRefID)}, nil
+}
+
+// used to extract SPDXRef values only from an SPDX Identifier which can point
+// to this document only. Use extractDocElementID for parsing IDs that can
+// refer either to this document or a different one.
+func ExtractElementID(value string) (common.ElementID, error) {
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(value, "SPDXRef-") {
+ return common.ElementID(""), fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(value, ":") {
+ return common.ElementID(""), fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(value, "SPDXRef-")
+ if eltRefID == "" {
+ return common.ElementID(""), fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.ElementID(eltRefID), nil
+}
+
+// used to extract key / value from embedded substrings
+// returns subkey, subvalue, nil if no error, or "", "", error otherwise
+func ExtractSubs(value string, sep string) (string, string, error) {
+ // parse the value to see if it's a valid subvalue format
+ sp := strings.SplitN(value, sep, 2)
+ if len(sp) == 1 {
+ return "", "", fmt.Errorf("invalid subvalue format for %s (no %s found)", value, sep)
+ }
+
+ subkey := strings.TrimSpace(sp[0])
+ subvalue := strings.TrimSpace(sp[1])
+
+ return subkey, subvalue, nil
+}
diff --git a/rdfloader/parser2v2/utils_test.go b/rdfloader/parser2v2/utils_test.go
new file mode 100644
index 0000000..c0cc574
--- /dev/null
+++ b/rdfloader/parser2v2/utils_test.go
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+func Test_getLastPartOfURI(t *testing.T) {
+ // uri of type baseFragment#fragment
+ input := "baseFragment#fragment"
+ expectedOutput := "fragment"
+ output := getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+
+ // uri of type baseFragment/subFragment
+ input = "baseFragment/subFragment"
+ expectedOutput = "subFragment"
+ output = getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+
+ // neither of the case mustn't raise any error.
+ input = "www.github.com"
+ expectedOutput = input
+ output = getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+}
+
+func Test_isUriValid(t *testing.T) {
+ // TestCase 1: Valid Input URI
+ input := "https://www.github.com"
+ isValid := isUriValid(input)
+ if !isValid {
+ t.Errorf("valid input(%s) detected as invalid.", input)
+ }
+
+ // TestCase 2: Invalid Input URI
+ input = `http\:www.github.com`
+ isValid = isUriValid(input)
+ if isValid {
+ t.Errorf("invalid input(%s) detected as valid", input)
+ }
+}
+
+func Test_rdfParser2_2_nodeToTriples(t *testing.T) {
+ var parser *rdfParser2_2
+ var output, expectedOutput []*gordfParser.Triple
+
+ // TestCase 1: a nil node shouldn't raise any error or panic.
+ parser, _ = parserFromBodyContent(``)
+ output = parser.nodeToTriples(nil)
+ if output == nil {
+ t.Errorf("nil input should return an empty slice and not nil")
+ }
+ expectedOutput = []*gordfParser.Triple{}
+ if !reflect.DeepEqual(output, expectedOutput) {
+ t.Errorf("expected %+v, got %+v", expectedOutput, output)
+ }
+
+ // TestCase 2: node should be addressable based on the node content and not the pointer.
+ // It should allow new nodes same as the older ones to retrieve the associated triples.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum rdf:about="#checksum">
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>75068c26abbed3ad3980685bae21d7202d288317</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ newNode := &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#checksum",
+ }
+ output = parser.nodeToTriples(newNode)
+
+ // The output must have 3 triples:
+ // 1. newNode rdf:type Checksum
+ // 2. newNode spdx:algorithm http://spdx.org/rdf/terms#checksumAlgorithm_sha1
+ // 3. newNode spdx:checksumValue 75068c26abbed3ad3980685bae21d7202d288317
+ if len(output) != 3 {
+ t.Errorf("expected output to have 3 triples, got %d", len(output))
+ }
+}
+
+func Test_boolFromString(t *testing.T) {
+ // TestCase 1: Valid Input: "true"
+ // mustn't raise any error
+ input := "true"
+ val, err := boolFromString(input)
+ if err != nil {
+ t.Errorf("function raised an error for a valid input(%s): %s", input, err)
+ }
+ if val != true {
+ t.Errorf("invalid output. Expected %v, found %v", true, val)
+ }
+
+ // TestCase 2: Valid Input: "true"
+ // mustn't raise any error
+ input = "false"
+ val, err = boolFromString(input)
+ if err != nil {
+ t.Errorf("function raised an error for a valid input(%s): %s", input, err)
+ }
+ if val != false {
+ t.Errorf("invalid output. Expected %v, found %v", false, val)
+ }
+
+ // TestCase 3: invalid input: ""
+ // it must raise an error
+ input = ""
+ val, err = boolFromString(input)
+ if err == nil {
+ t.Errorf("invalid input should've raised an error")
+ }
+}
+
+func Test_getNodeTypeFromTriples(t *testing.T) {
+ var err error
+ var node *gordfParser.Node
+ var triples []*gordfParser.Triple
+ var nodeType, expectedNodeType string
+
+ // TestCase 1: nil node must raise an error because,
+ // nil nodes cannot be associated with any rdf:type attribute.
+ _, err = getNodeTypeFromTriples(triples, nil)
+ if err == nil {
+ t.Errorf("expected an error due to nil node, got %v", err)
+ }
+
+ // TestCase 2: none of the triples give information about the rdf:type of a node.
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "N0",
+ }
+ _, err = getNodeTypeFromTriples(triples, node)
+ if err == nil {
+ t.Errorf("expected an error saying no rdf:type found, got %v", err)
+ }
+
+ // TestCase 3: node is associated with exactly one rdf:type triples
+ typeTriple := &gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: RDF_TYPE,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/rdf/terms#Checksum",
+ },
+ }
+ triples = append(triples, typeTriple)
+ expectedNodeType = "http://spdx.org/rdf/terms#Checksum"
+ nodeType, err = getNodeTypeFromTriples(triples, node)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if nodeType != expectedNodeType {
+ t.Errorf("expected: %v, got: %v", nodeType, expectedNodeType)
+ }
+
+ // TestCase 4: node associated with more than one rdf:type triples must raise an error.
+ typeTriple = &gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: RDF_TYPE,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/rdf/terms#Snippet",
+ },
+ }
+ triples = append(triples, typeTriple)
+ _, err = getNodeTypeFromTriples(triples, node)
+ if err == nil {
+ t.Errorf("expected an error saying more than one rdf:type found, got %v", err)
+ }
+}
+
+// following tests are copy pasted from tvloader/parser2v2/util_test.go
+
+func TestCanExtractDocumentAndElementRefsFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractDocElementID(t, "SPDXRef-file1", false, "", "file1")
+ // test with valid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file2", false, "doc2", "file2")
+ // test with invalid ID in this document
+ helperForExtractDocElementID(t, "a:SPDXRef-file1", true, "", "")
+ helperForExtractDocElementID(t, "file1", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-file1:", true, "", "")
+ // test with invalid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:a", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:SPDXRef-file1", true, "", "")
+ // test with invalid formats
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file1:file2", true, "", "")
+}
+
+func helperForExtractDocElementID(t *testing.T, tst string, wantErr bool, wantDoc string, wantElt string) {
+ deID, err := ExtractDocElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if deID.DocumentRefID != wantDoc {
+ if wantDoc == "" {
+ t.Errorf("testing %v: want empty string for DocumentRefID, got %v", tst, deID.DocumentRefID)
+ } else {
+ t.Errorf("testing %v: want %v for DocumentRefID, got %v", tst, wantDoc, deID.DocumentRefID)
+ }
+ }
+ if deID.ElementRefID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, deID.ElementRefID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, deID.ElementRefID)
+ }
+ }
+}
+
+func TestCanExtractElementRefsOnlyFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractElementID(t, "SPDXRef-file1", false, "file1")
+ // test with valid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-file2", true, "")
+ // test with invalid ID in this document
+ helperForExtractElementID(t, "a:SPDXRef-file1", true, "")
+ helperForExtractElementID(t, "file1", true, "")
+ helperForExtractElementID(t, "SPDXRef-", true, "")
+ helperForExtractElementID(t, "SPDXRef-file1:", true, "")
+ // test with invalid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:a", true, "")
+ helperForExtractElementID(t, "DocumentRef-:", true, "")
+ helperForExtractElementID(t, "DocumentRef-:SPDXRef-file1", true, "")
+}
+
+func helperForExtractElementID(t *testing.T, tst string, wantErr bool, wantElt string) {
+ eID, err := ExtractElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if eID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, eID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, eID)
+ }
+ }
+}
+
+func TestCanExtractSubvalues(t *testing.T) {
+ subkey, subvalue, err := ExtractSubs("SHA1: abc123", ":")
+ if err != nil {
+ t.Errorf("got error when calling extractSubs: %v", err)
+ }
+ if subkey != "SHA1" {
+ t.Errorf("got %v for subkey", subkey)
+ }
+ if subvalue != "abc123" {
+ t.Errorf("got %v for subvalue", subvalue)
+ }
+}
+
+func TestReturnsErrorForInvalidSubvalueFormat(t *testing.T) {
+ _, _, err := ExtractSubs("blah", ":")
+ if err == nil {
+ t.Errorf("expected error when calling extractSubs for invalid format (0 colons), got nil")
+ }
+}
diff --git a/rdfloader/parser2v3/constants.go b/rdfloader/parser2v3/constants.go
new file mode 100644
index 0000000..44ca0d4
--- /dev/null
+++ b/rdfloader/parser2v3/constants.go
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import "github.com/spdx/gordf/rdfloader/parser"
+
+var (
+ // NAMESPACES
+ NS_SPDX = "http://spdx.org/rdf/terms#"
+ NS_RDFS = "http://www.w3.org/2000/01/rdf-schema#"
+ NS_RDF = parser.RDFNS
+ NS_PTR = "http://www.w3.org/2009/pointers#"
+ NS_DOAP = "http://usefulinc.com/ns/doap#"
+
+ // SPDX properties
+ SPDX_SPEC_VERSION = NS_SPDX + "specVersion"
+ SPDX_DATA_LICENSE = NS_SPDX + "dataLicense"
+ SPDX_NAME = NS_SPDX + "name"
+ SPDX_EXTERNAL_DOCUMENT_REF = NS_SPDX + "externalDocumentRef"
+ SPDX_LICENSE_LIST_VERSION = NS_SPDX + "licenseListVersion"
+ SPDX_CREATOR = NS_SPDX + "creator"
+ SPDX_CREATED = NS_SPDX + "created"
+ SPDX_REVIEWED = NS_SPDX + "reviewed"
+ SPDX_DESCRIBES_PACKAGE = NS_SPDX + "describesPackage"
+ SPDX_HAS_EXTRACTED_LICENSING_INFO = NS_SPDX + "hasExtractedLicensingInfo"
+ SPDX_RELATIONSHIP = NS_SPDX + "relationship"
+ SPDX_ANNOTATION = NS_SPDX + "annotation"
+ SPDX_COMMENT = NS_SPDX + "comment"
+ SPDX_CREATION_INFO = NS_SPDX + "creationInfo"
+ SPDX_CHECKSUM_ALGORITHM_SHA1 = NS_SPDX + "checksumAlgorithm_sha1"
+ SPDX_CHECKSUM_ALGORITHM_SHA256 = NS_SPDX + "checksumAlgorithm_sha256"
+ SPDX_CHECKSUM_ALGORITHM_MD5 = NS_SPDX + "checksumAlgorithm_md5"
+ SPDX_EXTERNAL_DOCUMENT_ID = NS_SPDX + "externalDocumentId"
+ SPDX_SPDX_DOCUMENT = NS_SPDX + "spdxDocument"
+ SPDX_SPDX_DOCUMENT_CAPITALIZED = NS_SPDX + "SpdxDocument"
+ SPDX_CHECKSUM = NS_SPDX + "checksum"
+ SPDX_CHECKSUM_CAPITALIZED = NS_SPDX + "Checksum"
+ SPDX_ANNOTATION_TYPE = NS_SPDX + "annotationType"
+ SPDX_ANNOTATION_TYPE_OTHER = NS_SPDX + "annotationType_other"
+ SPDX_ANNOTATION_TYPE_REVIEW = NS_SPDX + "annotationType_review"
+ SPDX_LICENSE_INFO_IN_FILE = NS_SPDX + "licenseInfoInFile"
+ SPDX_LICENSE_CONCLUDED = NS_SPDX + "licenseConcluded"
+ SPDX_LICENSE_COMMENTS = NS_SPDX + "licenseComments"
+ SPDX_COPYRIGHT_TEXT = NS_SPDX + "copyrightText"
+ SPDX_ARTIFACT_OF = NS_SPDX + "artifactOf"
+ SPDX_NOTICE_TEXT = NS_SPDX + "noticeText"
+ SPDX_FILE_CONTRIBUTOR = NS_SPDX + "fileContributor"
+ SPDX_FILE_DEPENDENCY = NS_SPDX + "fileDependency"
+ SPDX_FILE_TYPE = NS_SPDX + "fileType"
+ SPDX_FILE_NAME = NS_SPDX + "fileName"
+ SPDX_EXTRACTED_TEXT = NS_SPDX + "extractedText"
+ SPDX_LICENSE_ID = NS_SPDX + "licenseId"
+ SPDX_FILE = NS_SPDX + "File"
+ SPDX_PACKAGE = NS_SPDX + "Package"
+ SPDX_SPDX_ELEMENT = NS_SPDX + "SpdxElement"
+ SPDX_VERSION_INFO = NS_SPDX + "versionInfo"
+ SPDX_PACKAGE_FILE_NAME = NS_SPDX + "packageFileName"
+ SPDX_SUPPLIER = NS_SPDX + "supplier"
+ SPDX_ORIGINATOR = NS_SPDX + "originator"
+ SPDX_DOWNLOAD_LOCATION = NS_SPDX + "downloadLocation"
+ SPDX_FILES_ANALYZED = NS_SPDX + "filesAnalyzed"
+ SPDX_PACKAGE_VERIFICATION_CODE = NS_SPDX + "packageVerificationCode"
+ SPDX_SOURCE_INFO = NS_SPDX + "sourceInfo"
+ SPDX_LICENSE_INFO_FROM_FILES = NS_SPDX + "licenseInfoFromFiles"
+ SPDX_LICENSE_DECLARED = NS_SPDX + "licenseDeclared"
+ SPDX_SUMMARY = NS_SPDX + "summary"
+ SPDX_DESCRIPTION = NS_SPDX + "description"
+ SPDX_EXTERNAL_REF = NS_SPDX + "externalRef"
+ SPDX_HAS_FILE = NS_SPDX + "hasFile"
+ SPDX_PRIMARY_PACKAGE_PURPOSE = NS_SPDX + "primaryPackagePurpose"
+ SPDX_RELEASE_DATE = NS_SPDX + "releaseDate"
+ SPDX_BUILT_DATE = NS_SPDX + "builtDate"
+ SPDX_VALID_UNTIL_DATE = NS_SPDX + "validUntilDate"
+ SPDX_ATTRIBUTION_TEXT = NS_SPDX + "attributionText"
+ SPDX_PACKAGE_VERIFICATION_CODE_VALUE = NS_SPDX + "packageVerificationCodeValue"
+ SPDX_PACKAGE_VERIFICATION_CODE_EXCLUDED_FILE = NS_SPDX + "packageVerificationCodeExcludedFile"
+ SPDX_RELATED_SPDX_ELEMENT = NS_SPDX + "relatedSpdxElement"
+ SPDX_RELATIONSHIP_TYPE = NS_SPDX + "relationshipType"
+ SPDX_SNIPPET_FROM_FILE = NS_SPDX + "snippetFromFile"
+ SPDX_LICENSE_INFO_IN_SNIPPET = NS_SPDX + "licenseInfoInSnippet"
+ SPDX_RANGE = NS_SPDX + "range"
+ SPDX_REVIEWER = NS_SPDX + "reviewer"
+ SPDX_REVIEW_DATE = NS_SPDX + "reviewDate"
+ SPDX_SNIPPET = NS_SPDX + "Snippet"
+ SPDX_ALGORITHM = NS_SPDX + "algorithm"
+ SPDX_CHECKSUM_VALUE = NS_SPDX + "checksumValue"
+ SPDX_REFERENCE_CATEGORY = NS_SPDX + "referenceCategory"
+ SPDX_REFERENCE_CATEGORY_PACKAGE_MANAGER = NS_SPDX + "referenceCategory_packageManager"
+ SPDX_REFERENCE_CATEGORY_SECURITY = NS_SPDX + "referenceCategory_security"
+ SPDX_REFERENCE_CATEGORY_OTHER = NS_SPDX + "referenceCategory_other"
+
+ SPDX_REFERENCE_TYPE = NS_SPDX + "referenceType"
+ SPDX_REFERENCE_LOCATOR = NS_SPDX + "referenceLocator"
+ SPDX_ANNOTATION_DATE = NS_SPDX + "annotationDate"
+ SPDX_ANNOTATOR = NS_SPDX + "annotator"
+ SPDX_MEMBER = NS_SPDX + "member"
+ SPDX_DISJUNCTIVE_LICENSE_SET = NS_SPDX + "DisjunctiveLicenseSet"
+ SPDX_CONJUNCTIVE_LICENSE_SET = NS_SPDX + "ConjunctiveLicenseSet"
+ SPDX_EXTRACTED_LICENSING_INFO = NS_SPDX + "ExtractedLicensingInfo"
+ SPDX_SIMPLE_LICENSING_INFO = NS_SPDX + "SimpleLicensingInfo"
+ SPDX_NONE_CAPS = NS_SPDX + "NONE"
+ SPDX_NOASSERTION_CAPS = NS_SPDX + "NOASSERTION"
+ SPDX_NONE_SMALL = NS_SPDX + "none"
+ SPDX_NOASSERTION_SMALL = NS_SPDX + "noassertion"
+ SPDX_LICENSE = NS_SPDX + "License"
+ SPDX_LISTED_LICENSE = NS_SPDX + "ListedLicense"
+ SPDX_EXAMPLE = NS_SPDX + "example"
+ SPDX_IS_OSI_APPROVED = NS_SPDX + "isOsiApproved"
+ SPDX_STANDARD_LICENSE_TEMPLATE = NS_SPDX + "standardLicenseTemplate"
+ SPDX_IS_DEPRECATED_LICENSE_ID = NS_SPDX + "isDeprecatedLicenseId"
+ SPDX_IS_FSF_LIBRE = NS_SPDX + "isFsfLibre"
+ SPDX_LICENSE_TEXT = NS_SPDX + "licenseText"
+ SPDX_STANDARD_LICENSE_HEADER = NS_SPDX + "standardLicenseHeader"
+ SPDX_LICENSE_EXCEPTION_ID = NS_SPDX + "licenseExceptionId"
+ SPDX_LICENSE_EXCEPTION_TEXT = NS_SPDX + "licenseExceptionText"
+ SPDX_LICENSE_EXCEPTION = NS_SPDX + "licenseException"
+ SPDX_WITH_EXCEPTION_OPERATOR = NS_SPDX + "WithExceptionOperator"
+ SPDX_OR_LATER_OPERATOR = NS_SPDX + "OrLaterOperator"
+ SPDX_STANDARD_LICENSE_HEADER_TEMPLATE = NS_SPDX + "standardLicenseHeaderTemplate"
+
+ // RDFS properties
+ RDFS_COMMENT = NS_RDFS + "comment"
+ RDFS_SEE_ALSO = NS_RDFS + "seeAlso"
+
+ // RDF properties
+ RDF_TYPE = NS_RDF + "type"
+
+ // DOAP properties
+ DOAP_HOMEPAGE = NS_DOAP + "homepage"
+ DOAP_NAME = NS_DOAP + "name"
+
+ // PTR properties
+ PTR_START_END_POINTER = NS_PTR + "StartEndPointer"
+ PTR_START_POINTER = NS_PTR + "startPointer"
+ PTR_BYTE_OFFSET_POINTER = NS_PTR + "ByteOffsetPointer"
+ PTR_LINE_CHAR_POINTER = NS_PTR + "LineCharPointer"
+ PTR_REFERENCE = NS_PTR + "reference"
+ PTR_OFFSET = NS_PTR + "offset"
+ PTR_LINE_NUMBER = NS_PTR + "lineNumber"
+ PTR_END_POINTER = NS_PTR + "endPointer"
+
+ // prefixes
+ PREFIX_RELATIONSHIP_TYPE = "relationshipType_"
+)
+
+func AllRelationshipTypes() []string {
+ return []string{
+ "amendment", "ancestorOf", "buildDependencyOf", "buildToolOf",
+ "containedBy", "contains", "copyOf", "dataFile", "dataFileOf",
+ "dependencyManifestOf", "dependencyOf", "dependsOn", "descendantOf",
+ "describedBy", "describes", "devDependencyOf", "devToolOf",
+ "distributionArtifact", "documentation", "dynamicLink", "exampleOf",
+ "expandedFromArchive", "fileAdded", "fileDeleted", "fileModified",
+ "generatedFrom", "generates", "hasPrerequisite", "metafileOf",
+ "optionalComponentOf", "optionalDependencyOf", "other", "packageOf",
+ "patchApplied", "patchFor", "prerequisiteFor", "providedDependencyOf",
+ "runtimeDependencyOf", "staticLink", "testDependencyOf", "testOf",
+ "testToolOf", "testcaseOf", "variantOf",
+ }
+}
+
+func AllStandardLicenseIDS() []string {
+ return []string{
+ "0BSD", "389-exception", "AAL", "Abstyles", "Adobe-2006", "Adobe-Glyph",
+ "ADSL", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", "Afmparse",
+ "AGPL-1.0-only", "AGPL-1.0-or-later", "AGPL-1.0", "AGPL-3.0-only",
+ "AGPL-3.0-or-later", "AGPL-3.0", "Aladdin", "AMDPLPA", "AML", "AMPAS",
+ "ANTLR-PD", "Apache-1.0", "Apache-1.1", "Apache-2.0", "APAFML", "APL-1.0",
+ "APSL-1.0", "APSL-1.1", "APSL-1.2", "APSL-2.0", "Artistic-1.0-cl8",
+ "Artistic-1.0-Perl", "Artistic-1.0", "Artistic-2.0", "",
+ "Autoconf-exception-2.0", "Autoconf-exception-3.0", "Bahyph", "Barr",
+ "Beerware", "Bison-exception-2.2", "BitTorrent-1.0", "BitTorrent-1.1",
+ "blessing", "BlueOak-1.0.0", "Bootloader-exception", "Borceux", "BSD-1-Clause",
+ "BSD-2-Clause-FreeBSD", "BSD-2-Clause-NetBSD", "BSD-2-Clause-Patent",
+ "BSD-2-Clause-Views", "BSD-2-Clause", "BSD-3-Clause-Attribution",
+ "BSD-3-Clause-Clear", "BSD-3-Clause-LBNL",
+ "BSD-3-Clause-No-Nuclear-License-2014", "BSD-3-Clause-No-Nuclear-License",
+ "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause-Open-MPI", "BSD-3-Clause",
+ "BSD-4-Clause-UC", "BSD-4-Clause", "BSD-Protection", "BSD-Source-Code",
+ "BSL-1.0", "bzip2-1.0.5", "bzip2-1.0.6", "CAL-1.0-Combined-Work-Exception",
+ "CAL-1.0", "Caldera", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5",
+ "CC-BY-3.0-AT", "CC-BY-3.0", "CC-BY-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0",
+ "CC-BY-NC-2.5", "CC-BY-NC-3.0", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0",
+ "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0-IGO", "CC-BY-NC-ND-3.0",
+ "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", "CC-BY-NC-SA-2.5",
+ "CC-BY-NC-SA-3.0", "CC-BY-NC-SA-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0",
+ "CC-BY-ND-2.5", "CC-BY-ND-3.0", "CC-BY-ND-4.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0",
+ "CC-BY-SA-2.5", "CC-BY-SA-3.0-AT", "CC-BY-SA-3.0", "CC-BY-SA-4.0", "CC-PDDC",
+ "CC0-1.0", "CDDL-1.0", "CDDL-1.1", "CDLA-Permissive-1.0", "CDLA-Sharing-1.0",
+ "CECILL-1.0", "CECILL-1.1", "CECILL-2.0", "CECILL-2.1", "CECILL-B", "CECILL-C",
+ "CERN-OHL-1.1", "CERN-OHL-1.2", "CERN-OHL-P-2.0", "CERN-OHL-S-2.0",
+ "CERN-OHL-W-2.0", "ClArtistic", "Classpath-exception-2.0",
+ "CLISP-exception-2.0", "CNRI-Jython", "CNRI-Python-GPL-Compatible",
+ "CNRI-Python", "Condor-1.1", "copyleft-next-0.3.0", "copyleft-next-0.3.1",
+ "CPAL-1.0", "CPL-1.0", "CPOL-1.02", "Crossword", "CrystalStacker",
+ "CUA-OPL-1.0", "Cube", "curl", "D-FSL-1.0", "diffmark",
+ "DigiRule-FOSS-exception", "DOC", "Dotseqn", "DSDP", "dvipdfm", "ECL-1.0",
+ "ECL-2.0", "eCos-2.0", "eCos-exception-2.0", "EFL-1.0", "EFL-2.0", "eGenix",
+ "Entessa", "EPICS", "EPL-1.0", "EPL-2.0", "ErlPL-1.1", "etalab-2.0",
+ "EUDatagrid", "EUPL-1.0", "EUPL-1.1", "EUPL-1.2", "Eurosym", "Fair",
+ "Fawkes-Runtime-exception", "FLTK-exception", "Font-exception-2.0",
+ "Frameworx-1.0", "FreeImage", "freertos-exception-2.0", "FSFAP", "FSFUL",
+ "FSFULLR", "FTL", "GCC-exception-2.0", "GCC-exception-3.1",
+ "GFDL-1.1-invariants-only", "GFDL-1.1-invariants-or-later",
+ "GFDL-1.1-no-invariants-only", "GFDL-1.1-no-invariants-or-later",
+ "GFDL-1.1-only", "GFDL-1.1-or-later", "GFDL-1.1", "GFDL-1.2-invariants-only",
+ "GFDL-1.2-invariants-or-later", "GFDL-1.2-no-invariants-only",
+ "GFDL-1.2-no-invariants-or-later", "GFDL-1.2-only", "GFDL-1.2-or-later",
+ "GFDL-1.2", "GFDL-1.3-invariants-only", "GFDL-1.3-invariants-or-later",
+ "GFDL-1.3-no-invariants-only", "GFDL-1.3-no-invariants-or-later",
+ "GFDL-1.3-only", "GFDL-1.3-or-later", "GFDL-1.3", "Giftware", "GL2PS", "Glide",
+ "Glulxe", "GLWTPL", "gnu-javamail-exception", "gnuplot", "GPL-1.0+",
+ "GPL-1.0-only", "GPL-1.0-or-later", "GPL-1.0", "GPL-2.0+", "GPL-2.0-only",
+ "GPL-2.0-or-later", "GPL-2.0-with-autoconf-exception",
+ "GPL-2.0-with-bison-exception", "GPL-2.0-with-classpath-exception",
+ "GPL-2.0-with-font-exception", "GPL-2.0-with-GCC-exception", "GPL-2.0",
+ "GPL-3.0+", "GPL-3.0-linking-exception", "GPL-3.0-linking-source-exception",
+ "GPL-3.0-only", "GPL-3.0-or-later", "GPL-3.0-with-autoconf-exception",
+ "GPL-3.0-with-GCC-exception", "GPL-3.0", "GPL-CC-1.0", "gSOAP-1.3b",
+ "HaskellReport", "Hippocratic-2.1", "HPND-sell-variant", "HPND",
+ "i2p-gpl-java-exception", "IBM-pibs", "ICU", "IJG", "ImageMagick", "iMatix",
+ "Imlib2", "Info-ZIP", "Intel-ACPI", "Intel", "Interbase-1.0", "IPA", "IPL-1.0",
+ "ISC", "JasPer-2.0", "JPNIC", "JSON", "LAL-1.2", "LAL-1.3", "Latex2e",
+ "Leptonica", "LGPL-2.0+", "LGPL-2.0-only", "LGPL-2.0-or-later", "LGPL-2.0",
+ "LGPL-2.1+", "LGPL-2.1-only", "LGPL-2.1-or-later", "LGPL-2.1", "LGPL-3.0+",
+ "LGPL-3.0-linking-exception", "LGPL-3.0-only", "LGPL-3.0-or-later", "LGPL-3.0",
+ "LGPLLR", "libpng-2.0", "Libpng", "libselinux-1.0", "libtiff",
+ "Libtool-exception", "licenses", "LiLiQ-P-1.1", "LiLiQ-R-1.1",
+ "LiLiQ-Rplus-1.1", "Linux-OpenIB", "Linux-syscall-note", "LLVM-exception",
+ "LPL-1.0", "LPL-1.02", "LPPL-1.0", "LPPL-1.1", "LPPL-1.2", "LPPL-1.3a",
+ "LPPL-1.3c", "LZMA-exception", "MakeIndex", "mif-exception", "MirOS", "MIT-0",
+ "MIT-advertising", "MIT-CMU", "MIT-enna", "MIT-feh", "MIT", "MITNFA",
+ "Motosoto", "mpich2", "MPL-1.0", "MPL-1.1", "MPL-2.0-no-copyleft-exception",
+ "MPL-2.0", "MS-PL", "MS-RL", "MTLL", "MulanPSL-1.0", "MulanPSL-2.0", "Multics",
+ "Mup", "NASA-1.3", "Naumen", "NBPL-1.0", "NCGL-UK-2.0", "NCSA", "Net-SNMP",
+ "NetCDF", "Newsletr", "NGPL", "NIST-PD-fallback", "NIST-PD", "NLOD-1.0",
+ "NLPL", "Nokia-Qt-exception-1.1", "Nokia", "NOSL", "Noweb", "NPL-1.0",
+ "NPL-1.1", "NPOSL-3.0", "NRL", "NTP-0", "NTP", "Nunit", "O-UDA-1.0",
+ "OCaml-LGPL-linking-exception", "OCCT-exception-1.0", "OCCT-PL", "OCLC-2.0",
+ "ODbL-1.0", "ODC-By-1.0", "OFL-1.0-no-RFN", "OFL-1.0-RFN", "OFL-1.0",
+ "OFL-1.1-no-RFN", "OFL-1.1-RFN", "OFL-1.1", "OGC-1.0", "OGL-Canada-2.0",
+ "OGL-UK-1.0", "OGL-UK-2.0", "OGL-UK-3.0", "OGTSL", "OLDAP-1.1", "OLDAP-1.2",
+ "OLDAP-1.3", "OLDAP-1.4", "OLDAP-2.0.1", "OLDAP-2.0", "OLDAP-2.1",
+ "OLDAP-2.2.1", "OLDAP-2.2.2", "OLDAP-2.2", "OLDAP-2.3", "OLDAP-2.4",
+ "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OLDAP-2.8", "OML", "",
+ "OpenJDK-assembly-exception-1.0", "OpenSSL", "openvpn-openssl-exception",
+ "OPL-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", "OSL-2.1",
+ "OSL-3.0", "Parity-6.0.0", "Parity-7.0.0", "PDDL-1.0", "PHP-3.0", "PHP-3.01",
+ "Plexus", "PolyForm-Noncommercial-1.0.0", "PolyForm-Small-Business-1.0.0",
+ "PostgreSQL", "PS-or-PDF-font-exception-20170817", "PSF-2.0", "psfrag",
+ "psutils", "Python-2.0", "Qhull", "QPL-1.0", "Qt-GPL-exception-1.0",
+ "Qt-LGPL-exception-1.1", "Qwt-exception-1.0", "Rdisc", "RHeCos-1.1", "RPL-1.1",
+ "RPL-1.5", "RPSL-1.0", "RSA-MD", "RSCPL", "Ruby", "SAX-PD", "Saxpath", "SCEA",
+ "Sendmail-8.23", "Sendmail", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", "SHL-0.5",
+ "SHL-0.51", "SHL-2.0", "SHL-2.1", "SimPL-2.0", "SISSL-1.2", "SISSL",
+ "Sleepycat", "SMLNJ", "SMPPL", "SNIA", "Spencer-86", "Spencer-94",
+ "Spencer-99", "SPL-1.0", "SSH-OpenSSH", "SSH-short", "SSPL-1.0",
+ "StandardML-NJ", "SugarCRM-1.1.3", "Swift-exception", "SWL", "TAPR-OHL-1.0",
+ "TCL", "TCP-wrappers", "TMate", "TORQUE-1.1", "TOSL", "TU-Berlin-1.0",
+ "TU-Berlin-2.0", "u-boot-exception-2.0", "UCL-1.0", "Unicode-DFS-2015",
+ "Unicode-DFS-2016", "Unicode-TOU", "Universal-FOSS-exception-1.0", "Unlicense",
+ "UPL-1.0", "Vim", "VOSTROM", "VSL-1.0", "W3C-19980720", "W3C-20150513", "W3C",
+ "Watcom-1.0", "Wsuipa", "WTFPL", "WxWindows-exception-3.1", "wxWindows", "X11",
+ "Xerox", "XFree86-1.1", "xinetd", "Xnet", "xpp", "XSkat", "YPL-1.0", "YPL-1.1",
+ "Zed", "Zend-2.0", "Zimbra-1.3", "Zimbra-1.4", "zlib-acknowledgement", "Zlib",
+ "ZPL-1.1", "ZPL-2.0", "ZPL-2.1",
+ }
+}
diff --git a/rdfloader/parser2v3/license_utils.go b/rdfloader/parser2v3/license_utils.go
new file mode 100644
index 0000000..11bb3c8
--- /dev/null
+++ b/rdfloader/parser2v3/license_utils.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+/* util methods for licenses and checksums below:*/
+
+// Given the license URI, returns the name of the license defined
+// in the last part of the uri.
+// This function is susceptible to false-positives.
+func getLicenseStringFromURI(uri string) string {
+ licenseEnd := strings.TrimSpace(getLastPartOfURI(uri))
+ lower := strings.ToLower(licenseEnd)
+ if lower == "none" || lower == "noassertion" {
+ return strings.ToUpper(licenseEnd)
+ }
+ return licenseEnd
+}
+
+// returns the checksum algorithm and it's value
+// In the newer versions, these two strings will be bound to a single checksum struct
+// whose pointer will be returned.
+func (parser *rdfParser2_3) getChecksumFromNode(checksumNode *gordfParser.Node) (algorithm common.ChecksumAlgorithm, value string, err error) {
+ var checksumValue, checksumAlgorithm string
+ for _, checksumTriple := range parser.nodeToTriples(checksumNode) {
+ switch checksumTriple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_CHECKSUM_VALUE:
+ // cardinality: exactly 1
+ checksumValue = strings.TrimSpace(checksumTriple.Object.ID)
+ case SPDX_ALGORITHM:
+ // cardinality: exactly 1
+ checksumAlgorithm, err = getAlgorithmFromURI(checksumTriple.Object.ID)
+ if err != nil {
+ return
+ }
+ default:
+ err = fmt.Errorf("unknown predicate '%s' while parsing checksum node", checksumTriple.Predicate.ID)
+ return
+ }
+ }
+ return common.ChecksumAlgorithm(checksumAlgorithm), checksumValue, nil
+}
+
+func getAlgorithmFromURI(algorithmURI string) (checksumAlgorithm string, err error) {
+ fragment := getLastPartOfURI(algorithmURI)
+ if !strings.HasPrefix(fragment, "checksumAlgorithm_") {
+ return "", fmt.Errorf("checksum algorithm uri must begin with checksumAlgorithm_. found %s", fragment)
+ }
+ algorithm := strings.TrimPrefix(fragment, "checksumAlgorithm_")
+ algorithm = strings.ToLower(strings.TrimSpace(algorithm))
+ switch algorithm {
+ case "md2", "md4", "md5", "md6":
+ checksumAlgorithm = strings.ToUpper(algorithm)
+ case "sha1", "sha224", "sha256", "sha384", "sha512":
+ checksumAlgorithm = strings.ToUpper(algorithm)
+ default:
+ return "", fmt.Errorf("unknown checksum algorithm %s", algorithm)
+ }
+ return
+}
+
+// from a list of licenses, it returns a
+// list of string representation of those licenses.
+func mapLicensesToStrings(licences []AnyLicenseInfo) []string {
+ res := make([]string, len(licences), len(licences))
+ for i, lic := range licences {
+ res[i] = lic.ToLicenseString()
+ }
+ return res
+}
+
+/****** Type Functions ******/
+
+// TODO: should probably add brackets while linearizing a nested license.
+func (lic ConjunctiveLicenseSet) ToLicenseString() string {
+ return strings.Join(mapLicensesToStrings(lic.members), " AND ")
+}
+
+// TODO: should probably add brackets while linearizing a nested license.
+func (lic DisjunctiveLicenseSet) ToLicenseString() string {
+ return strings.Join(mapLicensesToStrings(lic.members), " OR ")
+}
+
+func (lic ExtractedLicensingInfo) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (operator OrLaterOperator) ToLicenseString() string {
+ return operator.member.ToLicenseString()
+}
+
+func (lic License) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (lic ListedLicense) ToLicenseString() string {
+ return lic.licenseID
+}
+
+func (lic WithExceptionOperator) ToLicenseString() string {
+ return lic.member.ToLicenseString()
+}
+
+func (lic SpecialLicense) ToLicenseString() string {
+ return string(lic.value)
+}
+
+func (lic SimpleLicensingInfo) ToLicenseString() string {
+ return lic.licenseID
+}
diff --git a/rdfloader/parser2v3/license_utils_test.go b/rdfloader/parser2v3/license_utils_test.go
new file mode 100644
index 0000000..0156b99
--- /dev/null
+++ b/rdfloader/parser2v3/license_utils_test.go
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+)
+
+func Test_getLicenseStringFromURI(t *testing.T) {
+ // TestCase 1: NONE license
+ input := SPDX_NONE_CAPS
+ output := getLicenseStringFromURI(input)
+ expectedOutput := "NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+
+ // TestCase 2: NOASSERTION license
+ input = SPDX_NOASSERTION_SMALL
+ output = getLicenseStringFromURI(input)
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+
+ // TestCase 3: Other license
+ input = NS_SPDX + "LicenseRef-1"
+ output = getLicenseStringFromURI(input)
+ expectedOutput = "LicenseRef-1"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", expectedOutput, output)
+ }
+}
+
+func Test_rdfParser2_3_getChecksumFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ // TestCase 1: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ `)
+ checksumNode := parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getChecksumFromNode(checksumNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid checksum algorithm")
+ }
+
+ // TestCase 2: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ <spdx:invalidPredicate />
+ </spdx:Checksum>
+ `)
+ checksumNode = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getChecksumFromNode(checksumNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate")
+ }
+
+ // TestCase 3: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ `)
+ checksumNode = parser.gordfParserObj.Triples[0].Subject
+ algorithm, value, err := parser.getChecksumFromNode(checksumNode)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if algorithm != "SHA1" {
+ t.Errorf("expected checksum algorithm to be sha1, found %s", algorithm)
+ }
+ expectedValue := "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ if value != expectedValue {
+ t.Errorf("expected checksumValue to be %s, found %s", expectedValue, value)
+ }
+}
+
+func Test_rdfParser2_3_getAlgorithmFromURI(t *testing.T) {
+ var algorithmURI string
+ var err error
+
+ // TestCase 1: checksumAlgorithm uri doesn't start with checksumAlgorithm_
+ algorithmURI = NS_SPDX + "sha1"
+ _, err = getAlgorithmFromURI(algorithmURI)
+ if err == nil {
+ t.Errorf("should've raised an error for algorithmURI that doesn't start with checksumAlgorithm_")
+ }
+
+ // TestCase 2: unknown checksum algorithm
+ algorithmURI = NS_SPDX + "checksumAlgorithm_sha999"
+ _, err = getAlgorithmFromURI(algorithmURI)
+ if err == nil {
+ t.Errorf("should've raised an error for invalid algorithm")
+ }
+
+ // TestCase 3: valid input
+ algorithmURI = NS_SPDX + "checksumAlgorithm_sha256"
+ algorithm, err := getAlgorithmFromURI(algorithmURI)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if algorithm != "SHA256" {
+ t.Errorf("expected: SHA256, found: %s", algorithm)
+ }
+}
+
+func Test_mapLicensesToStrings(t *testing.T) {
+ // nothing much to test here.
+ // just a dummy dry run.
+ licenses := []AnyLicenseInfo{
+ SpecialLicense{
+ value: NONE,
+ },
+ SpecialLicense{
+ value: NOASSERTION,
+ },
+ }
+ licenseStrings := mapLicensesToStrings(licenses)
+ expectedLicenseStrings := []string{"NONE", "NOASSERTION"}
+ if !reflect.DeepEqual(licenseStrings, expectedLicenseStrings) {
+ t.Errorf("expected: %+v\nfound %+v", expectedLicenseStrings, licenseStrings)
+ }
+}
+
+func TestConjunctiveLicenseSet_ToLicenseString(t *testing.T) {
+ var lic ConjunctiveLicenseSet
+ var output, expectedOutput string
+
+ // TestCase 1: no license in the set
+ lic = ConjunctiveLicenseSet{
+ members: nil,
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = ""
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 2: single license in the set
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 3: more than one license in the set.
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ SpecialLicense{value: NONE},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION AND NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 4: nested conjunctive license.
+ lic = ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: "LicenseRef-1"},
+ SpecialLicense{value: NONE},
+ },
+ },
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION AND LicenseRef-1 AND NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+}
+
+func TestDisjunctiveLicenseSet_ToLicenseString(t *testing.T) {
+ var lic DisjunctiveLicenseSet
+ var output, expectedOutput string
+
+ // TestCase 1: no license in the set
+ lic = DisjunctiveLicenseSet{
+ members: nil,
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = ""
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 2: single license in the set
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 3: more than one license in the set.
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ SpecialLicense{value: NONE},
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION OR NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+
+ // TestCase 4: nested conjunctive license.
+ lic = DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: NOASSERTION},
+ DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{
+ SpecialLicense{value: "LicenseRef-1"},
+ SpecialLicense{value: NONE},
+ },
+ },
+ },
+ }
+ output = lic.ToLicenseString()
+ expectedOutput = "NOASSERTION OR LicenseRef-1 OR NONE"
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found %s", output, expectedOutput)
+ }
+}
+
+func TestExtractedLicensingInfo_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ extractedLicense := ExtractedLicensingInfo{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ extractedText: "extracted Text",
+ }
+ expectedOutput := "license"
+ output := extractedLicense.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestOrLaterOperator_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ orLater := OrLaterOperator{
+ member: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ }
+ expectedOutput := "license"
+ output := orLater.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ license := License{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ }
+ expectedOutput := "license"
+ output := license.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestListedLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ ll := ListedLicense{License{
+ SimpleLicensingInfo: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ },
+ }
+ expectedOutput := "license"
+ output := ll.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestWithExceptionOperator_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ withException := WithExceptionOperator{
+ member: SimpleLicensingInfo{
+ licenseID: "license",
+ },
+ licenseException: LicenseException{},
+ }
+ expectedOutput := "license"
+ output := withException.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestSpecialLicense_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ specialLicense := SpecialLicense{
+ value: "license",
+ }
+ expectedOutput := "license"
+ output := specialLicense.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
+
+func TestSimpleLicensingInfo_ToLicenseString(t *testing.T) {
+ // nothing to test (just a dry run)
+ sli := SimpleLicensingInfo{
+ licenseID: "license",
+ }
+ expectedOutput := "license"
+ output := sli.ToLicenseString()
+ if output != expectedOutput {
+ t.Errorf("expected: %s, found: %s", expectedOutput, output)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_annotation.go b/rdfloader/parser2v3/parse_annotation.go
new file mode 100644
index 0000000..507158a
--- /dev/null
+++ b/rdfloader/parser2v3/parse_annotation.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// creates a new instance of annotation and sets the annotation attributes
+// associated with the given node.
+// The newly created annotation is appended to the doc.
+func (parser *rdfParser2_3) parseAnnotationFromNode(node *gordfParser.Node) (err error) {
+ ann := &v2_3.Annotation{}
+ for _, subTriple := range parser.nodeToTriples(node) {
+ switch subTriple.Predicate.ID {
+ case SPDX_ANNOTATOR:
+ // cardinality: exactly 1
+ err = setAnnotatorFromString(subTriple.Object.ID, ann)
+ case SPDX_ANNOTATION_DATE:
+ // cardinality: exactly 1
+ ann.AnnotationDate = subTriple.Object.ID
+ case RDFS_COMMENT:
+ // cardinality: exactly 1
+ ann.AnnotationComment = subTriple.Object.ID
+ case SPDX_ANNOTATION_TYPE:
+ // cardinality: exactly 1
+ err = setAnnotationType(subTriple.Object.ID, ann)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ default:
+ err = fmt.Errorf("unknown predicate %s while parsing annotation", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return setAnnotationToParser(parser, ann)
+}
+
+func setAnnotationToParser(parser *rdfParser2_3, annotation *v2_3.Annotation) error {
+ if parser.doc == nil {
+ return errors.New("uninitialized spdx document")
+ }
+ if parser.doc.Annotations == nil {
+ parser.doc.Annotations = []*v2_3.Annotation{}
+ }
+ parser.doc.Annotations = append(parser.doc.Annotations, annotation)
+ return nil
+}
+
+// annotator is of type [Person|Organization|Tool]:String
+func setAnnotatorFromString(annotatorString string, ann *v2_3.Annotation) error {
+ subkey, subvalue, err := ExtractSubs(annotatorString, ":")
+ if err != nil {
+ return err
+ }
+ if subkey == "Person" || subkey == "Organization" || subkey == "Tool" {
+ ann.Annotator.AnnotatorType = subkey
+ ann.Annotator.Annotator = subvalue
+ return nil
+ }
+ return fmt.Errorf("unrecognized Annotator type %v while parsing annotation", subkey)
+}
+
+// it can be NS_SPDX+annotationType_[review|other]
+func setAnnotationType(annType string, ann *v2_3.Annotation) error {
+ switch annType {
+ case SPDX_ANNOTATION_TYPE_OTHER:
+ ann.AnnotationType = "OTHER"
+ case SPDX_ANNOTATION_TYPE_REVIEW:
+ ann.AnnotationType = "REVIEW"
+ default:
+ return fmt.Errorf("unknown annotation type %s", annType)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_annotation_test.go b/rdfloader/parser2v3/parse_annotation_test.go
new file mode 100644
index 0000000..1ce0bd7
--- /dev/null
+++ b/rdfloader/parser2v3/parse_annotation_test.go
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_setAnnotatorFromString(t *testing.T) {
+ // TestCase 1: Empty String must raise an error
+ ann := &v2_3.Annotation{}
+ input := ""
+ err := setAnnotatorFromString(input, ann)
+ if err == nil {
+ t.Error("should've raised an error for an empty string")
+ }
+
+ // TestCase 2: Invalid annotator type
+ ann = &v2_3.Annotation{}
+ input = "Company: some_company"
+ err = setAnnotatorFromString(input, ann)
+ if err == nil {
+ t.Errorf("should've raised an error for an unknown annotator type")
+ }
+
+ // TestCase 3: Valid annotator
+ ann = &v2_3.Annotation{}
+ input = "Person: Rishabh"
+ err = setAnnotatorFromString(input, ann)
+ if err != nil {
+ t.Errorf("unexpected error for a valid annotator")
+ }
+ if ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf("wrnog annotator type: expected: %s, found: %s", "Person", ann.Annotator)
+ }
+ if ann.Annotator.Annotator != "Rishabh" {
+ t.Errorf("wrong annotator: expected: %s, found: %s", "Rishabh", ann.Annotator)
+ }
+}
+
+func Test_setAnnotationType(t *testing.T) {
+ ann := &v2_3.Annotation{}
+ // TestCase 1: invalid input (empty annotationType)
+ err := setAnnotationType("", ann)
+ if err == nil {
+ t.Errorf("expected an error for empty input")
+ }
+
+ // TestCase 2: invalid input (unknown annotation type)
+ err = setAnnotationType(NS_SPDX+"annotationType_unknown", ann)
+ if err == nil {
+ t.Errorf("expected an error for invalid annotationType")
+ }
+
+ // TestCase 3: valid input (annotationType_other)
+ err = setAnnotationType(SPDX_ANNOTATION_TYPE_OTHER, ann)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ann.AnnotationType != "OTHER" {
+ t.Errorf("expected: OTHER, found: %s", ann.AnnotationType)
+ }
+
+ // TestCase 4: valid input (annotationType_review)
+ err = setAnnotationType(SPDX_ANNOTATION_TYPE_REVIEW, ann)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ann.AnnotationType != "REVIEW" {
+ t.Errorf("expected: REVIEW, found: %s", ann.AnnotationType)
+ }
+}
+
+func Test_setAnnotationToParser(t *testing.T) {
+ // TestCase 1: doc is nil (must raise an error)
+ parser, _ := parserFromBodyContent(``)
+ parser.doc = nil
+ err := setAnnotationToParser(parser, &v2_3.Annotation{})
+ if err == nil {
+ t.Errorf("empty doc should've raised an error")
+ }
+
+ // TestCase 2: empty annotations should create a new annotations
+ // list and append the input to it.
+ parser, _ = parserFromBodyContent(``)
+ parser.doc.Annotations = nil
+ err = setAnnotationToParser(parser, &v2_3.Annotation{})
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if len(parser.doc.Annotations) != 1 {
+ t.Errorf("expected doc to have 1 annotation, found %d", len(parser.doc.Annotations))
+ }
+}
+
+func Test_rdfParser2_3_parseAnnotationFromNode(t *testing.T) {
+ // TestCase 1: invalid annotator must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Company: some company</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ err := parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("wrong annotator type should've raised an error")
+ }
+
+ // TestCase 2: wrong annotation type should raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_unknown"/>
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("wrong annotation type should've raised an error")
+ }
+
+ // TestCase 3: unknown predicate should also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_unknown"/>
+ <spdx:unknownPredicate />
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err == nil {
+ t.Errorf("unknown predicate must raise an error")
+ }
+
+ // TestCase 4: completely valid annotation
+ parser, _ = parserFromBodyContent(`
+ <spdx:Annotation>
+ <spdx:annotationDate>2010-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Document level annotation</rdfs:comment>
+ <spdx:annotator>Person: Jane Doe</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseAnnotationFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing valid a annotation")
+ }
+ if n := len(parser.doc.Annotations); n != 1 {
+ t.Errorf("document should've had only one annotation, found %d", n)
+ }
+ ann := parser.doc.Annotations[0]
+ // validating all the attributes of the annotations
+ expectedComment := "Document level annotation"
+ if ann.AnnotationComment != expectedComment {
+ t.Errorf(`expected: "%s", found "%s"`, expectedComment, ann.AnnotationComment)
+ }
+ expectedDate := "2010-01-29T18:30:22Z"
+ if expectedDate != ann.AnnotationDate {
+ t.Errorf(`expected: "%s", found "%s"`, expectedDate, ann.AnnotationDate)
+ }
+ expectedAnnotator := "Jane Doe"
+ if expectedAnnotator != ann.Annotator.Annotator {
+ t.Errorf(`expected: "%s", found "%s"`, expectedAnnotator, ann.Annotator)
+ }
+ if ann.Annotator.AnnotatorType != "Person" {
+ t.Errorf(`expected: "%s", found "%s"`, "Person", ann.Annotator.AnnotatorType)
+ }
+ expectedAnnotationType := "OTHER"
+ if expectedAnnotationType != ann.AnnotationType {
+ t.Errorf(`expected: "%s", found "%s"`, expectedAnnotationType, ann.AnnotationType)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_creation_info.go b/rdfloader/parser2v3/parse_creation_info.go
new file mode 100644
index 0000000..93a5628
--- /dev/null
+++ b/rdfloader/parser2v3/parse_creation_info.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// Cardinality: Mandatory, one.
+func (parser *rdfParser2_3) parseCreationInfoFromNode(ci *v2_3.CreationInfo, node *gordfParser.Node) error {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_LICENSE_LIST_VERSION: // 2.7
+ // cardinality: max 1
+ ci.LicenseListVersion = triple.Object.ID
+ case SPDX_CREATOR: // 2.8
+ // cardinality: min 1
+ err := setCreator(triple.Object.ID, ci)
+ if err != nil {
+ return err
+ }
+ case SPDX_CREATED: // 2.9
+ // cardinality: exactly 1
+ ci.Created = triple.Object.ID
+ case RDFS_COMMENT: // 2.10
+ ci.CreatorComment = triple.Object.ID
+ case RDF_TYPE:
+ continue
+ default:
+ return fmt.Errorf("unknown predicate %v while parsing a creation info", triple.Predicate)
+ }
+ }
+ return nil
+}
+
+func setCreator(creatorStr string, ci *v2_3.CreationInfo) error {
+ entityType, entity, err := ExtractSubs(creatorStr, ":")
+ if err != nil {
+ return fmt.Errorf("error setting creator of a creation info: %s", err)
+ }
+
+ creator := common.Creator{Creator: entity}
+
+ switch entityType {
+ case "Person", "Organization", "Tool":
+ creator.CreatorType = entityType
+ default:
+ return fmt.Errorf("unknown creatorType %v in a creation info", entityType)
+ }
+
+ ci.Creators = append(ci.Creators, creator)
+
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_creation_info_test.go b/rdfloader/parser2v3/parse_creation_info_test.go
new file mode 100644
index 0000000..354ac73
--- /dev/null
+++ b/rdfloader/parser2v3/parse_creation_info_test.go
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_setCreator(t *testing.T) {
+ // TestCase 1: invalid creator (empty)
+ input := ""
+ err := setCreator(input, &v2_3.CreationInfo{})
+ if err == nil {
+ t.Errorf("shoud've raised an error due to invalid input")
+ }
+
+ // TestCase 2: invalid entity type
+ input = "Company: some company"
+ err = setCreator(input, &v2_3.CreationInfo{})
+ if err == nil {
+ t.Errorf("shoud've raised an error due to unknown entity type")
+ }
+
+ // TestCase 3: valid input
+ input = "Person: Jane Doe"
+ ci := &v2_3.CreationInfo{}
+ err = setCreator(input, ci)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ if len(ci.Creators) != 1 {
+ t.Errorf("creationInfo should've had 1 creatorPersons, found %d", len(ci.Creators))
+ }
+ expectedPerson := "Jane Doe"
+ if ci.Creators[0].Creator != expectedPerson {
+ t.Errorf("expected %s, found %s", expectedPerson, ci.Creators[0])
+ }
+}
+
+func Test_rdfParser2_3_parseCreationInfoFromNode(t *testing.T) {
+ // TestCase 1: invalid creator must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person Unknown</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ </spdx:CreationInfo>
+ `)
+ ciNode := parser.gordfParserObj.Triples[0].Subject
+ err := parser.parseCreationInfoFromNode(&v2_3.CreationInfo{}, ciNode)
+ if err == nil {
+ t.Errorf("invalid creator must raise an error")
+ }
+
+ // TestCase 2: unknown predicate must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: fossy (y)</spdx:creator>
+ <spdx:creator>Organization: </spdx:creator>
+ <spdx:creator>Tool: spdx2</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ <spdx:unknownPredicate />
+ </spdx:CreationInfo>
+ `)
+ ciNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseCreationInfoFromNode(&v2_3.CreationInfo{}, ciNode)
+ if err == nil {
+ t.Errorf("unknown predicate must raise an error")
+ }
+
+ // TestCase 2: unknown predicate must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: fossy</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:CreationInfo>
+ `)
+ ciNode = parser.gordfParserObj.Triples[0].Subject
+ ci := &v2_3.CreationInfo{}
+ err = parser.parseCreationInfoFromNode(ci, ciNode)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ci.LicenseListVersion != "2.6" {
+ t.Errorf(`expected %s, found %s`, "2.6", ci.LicenseListVersion)
+ }
+ n := len(ci.Creators)
+ if n != 1 {
+ t.Errorf("expected 1 creatorPersons, found %d", n)
+ }
+ if ci.Creators[0].Creator != "fossy" {
+ t.Errorf("expected %s, found %s", "fossy", ci.Creators[0].Creator)
+ }
+ expectedCreated := "2018-08-24T19:55:34Z"
+ if ci.Created != expectedCreated {
+ t.Errorf("expected %s, found %s", expectedCreated, ci.Created)
+ }
+ expectedComment := "comment"
+ if ci.CreatorComment != expectedComment {
+ t.Errorf("expected %s, found %s", expectedComment, ci.CreatorComment)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_file.go b/rdfloader/parser2v3/parse_file.go
new file mode 100644
index 0000000..54c92c1
--- /dev/null
+++ b/rdfloader/parser2v3/parse_file.go
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// returns a file instance and the error if any encountered.
+func (parser *rdfParser2_3) getFileFromNode(fileNode *gordfParser.Node) (file *v2_3.File, err error) {
+ file = &v2_3.File{}
+
+ currState := parser.cache[fileNode.ID]
+ if currState == nil {
+ // this is the first time we are seeing this node.
+ parser.cache[fileNode.ID] = &nodeState{
+ object: file,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this file node and we needn't parse it again.
+ return currState.object.(*v2_3.File), nil
+ }
+
+ // setting color to grey to indicate that we've started parsing this node.
+ parser.cache[fileNode.ID].Color = GREY
+
+ // setting color to black just before function returns to the caller to
+ // indicate that parsing current node is complete.
+ defer func() { parser.cache[fileNode.ID].Color = BLACK }()
+
+ err = setFileIdentifier(fileNode.ID, file) // 4.2
+ if err != nil {
+ return nil, err
+ }
+
+ if existingFile := parser.files[file.FileSPDXIdentifier]; existingFile != nil {
+ file = existingFile
+ }
+
+ for _, subTriple := range parser.nodeToTriples(fileNode) {
+ switch subTriple.Predicate.ID {
+ case SPDX_FILE_NAME: // 4.1
+ // cardinality: exactly 1
+ file.FileName = subTriple.Object.ID
+ case SPDX_NAME:
+ // cardinality: exactly 1
+ // TODO: check where it will be set in the golang-tools spdx-data-model
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ case SPDX_FILE_TYPE: // 4.3
+ // cardinality: min 0
+ fileType := ""
+ fileType, err = parser.getFileTypeFromUri(subTriple.Object.ID)
+ file.FileTypes = append(file.FileTypes, fileType)
+ case SPDX_CHECKSUM: // 4.4
+ // cardinality: min 1
+ err = parser.setFileChecksumFromNode(file, subTriple.Object)
+ case SPDX_LICENSE_CONCLUDED: // 4.5
+ // cardinality: (exactly 1 anyLicenseInfo) or (None) or (Noassertion)
+ anyLicense, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing licenseConcluded: %v", err)
+ }
+ file.LicenseConcluded = anyLicense.ToLicenseString()
+ case SPDX_LICENSE_INFO_IN_FILE: // 4.6
+ // cardinality: min 1
+ lic, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing licenseInfoInFile: %v", err)
+ }
+ file.LicenseInfoInFiles = append(file.LicenseInfoInFiles, lic.ToLicenseString())
+ case SPDX_LICENSE_COMMENTS: // 4.7
+ // cardinality: max 1
+ file.LicenseComments = subTriple.Object.ID
+ // TODO: allow copyright text to be of type NOASSERTION
+ case SPDX_COPYRIGHT_TEXT: // 4.8
+ // cardinality: exactly 1
+ file.FileCopyrightText = subTriple.Object.ID
+ case SPDX_LICENSE_INFO_FROM_FILES:
+ // TODO: implement it. It is not defined in the tools-golang model.
+ // deprecated artifactOf (see sections 4.9, 4.10, 4.11)
+ case SPDX_ARTIFACT_OF:
+ // cardinality: min 0
+ var artifactOf *v2_3.ArtifactOfProject
+ artifactOf, err = parser.getArtifactFromNode(subTriple.Object)
+ file.ArtifactOfProjects = append(file.ArtifactOfProjects, artifactOf)
+ case RDFS_COMMENT: // 4.12
+ // cardinality: max 1
+ file.FileComment = subTriple.Object.ID
+ case SPDX_NOTICE_TEXT: // 4.13
+ // cardinality: max 1
+ file.FileNotice = getNoticeTextFromNode(subTriple.Object)
+ case SPDX_FILE_CONTRIBUTOR: // 4.14
+ // cardinality: min 0
+ file.FileContributors = append(file.FileContributors, subTriple.Object.ID)
+ case SPDX_FILE_DEPENDENCY:
+ // cardinality: min 0
+ newFile, err := parser.getFileFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error setting a file dependency in a file: %v", err)
+ }
+ file.FileDependencies = append(file.FileDependencies, string(newFile.FileSPDXIdentifier))
+ case SPDX_ATTRIBUTION_TEXT:
+ // cardinality: min 0
+ file.FileAttributionTexts = append(file.FileAttributionTexts, subTriple.Object.ID)
+ case SPDX_ANNOTATION:
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ case SPDX_RELATIONSHIP:
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ default:
+ return nil, fmt.Errorf("unknown triple predicate id %s", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+ parser.files[file.FileSPDXIdentifier] = file
+ return file, nil
+}
+
+func (parser *rdfParser2_3) setFileChecksumFromNode(file *v2_3.File, checksumNode *gordfParser.Node) error {
+ checksumAlgorithm, checksumValue, err := parser.getChecksumFromNode(checksumNode)
+ if err != nil {
+ return fmt.Errorf("error parsing checksumNode of a file: %v", err)
+ }
+ if file.Checksums == nil {
+ file.Checksums = []common.Checksum{}
+ }
+ switch checksumAlgorithm {
+ case common.SHA1,
+ common.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6,
+ common.SHA3_256,
+ common.SHA3_384,
+ common.SHA3_512,
+ common.BLAKE2b_256,
+ common.BLAKE2b_384,
+ common.BLAKE2b_512,
+ common.BLAKE3,
+ common.ADLER32:
+ file.Checksums = append(file.Checksums, common.Checksum{Algorithm: checksumAlgorithm, Value: checksumValue})
+ case "":
+ return fmt.Errorf("empty checksum algorithm and value")
+ default:
+ return fmt.Errorf("unknown checksumAlgorithm %s for a file", checksumAlgorithm)
+ }
+ return nil
+}
+
+func (parser *rdfParser2_3) getArtifactFromNode(node *gordfParser.Node) (*v2_3.ArtifactOfProject, error) {
+ artifactOf := &v2_3.ArtifactOfProject{}
+ // setting artifactOfProjectURI attribute (which is optional)
+ if node.NodeType == gordfParser.IRI {
+ artifactOf.URI = node.ID
+ }
+ // parsing rest triples and attributes of the artifact.
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ case DOAP_HOMEPAGE:
+ artifactOf.HomePage = triple.Object.ID
+ case DOAP_NAME:
+ artifactOf.Name = triple.Object.ID
+ default:
+ return nil, fmt.Errorf("error parsing artifactOf predicate %s", triple.Predicate.ID)
+ }
+ }
+ return artifactOf, nil
+}
+
+// TODO: check if the filetype is valid.
+func (parser *rdfParser2_3) getFileTypeFromUri(uri string) (string, error) {
+ // fileType is given as a uri. for example: http://spdx.org/rdf/terms#fileType_text
+ lastPart := getLastPartOfURI(uri)
+ if !strings.HasPrefix(lastPart, "fileType_") {
+ return "", fmt.Errorf("fileType Uri must begin with fileTYpe_. found: %s", lastPart)
+ }
+ return strings.TrimPrefix(lastPart, "fileType_"), nil
+}
+
+// populates parser.doc.Files by a list of files which are not
+// associated with a package by the hasFile attribute
+// assumes: all the packages are already parsed.
+func (parser *rdfParser2_3) setUnpackagedFiles() {
+ for fileID := range parser.files {
+ if !parser.assocWithPackage[fileID] {
+ parser.doc.Files = append(parser.doc.Files, parser.files[fileID])
+ }
+ }
+}
+
+func setFileIdentifier(idURI string, file *v2_3.File) (err error) {
+ idURI = strings.TrimSpace(idURI)
+ uriFragment := getLastPartOfURI(idURI)
+ file.FileSPDXIdentifier, err = ExtractElementID(uriFragment)
+ if err != nil {
+ return fmt.Errorf("error setting file identifier: %s", err)
+ }
+ return nil
+}
+
+func getNoticeTextFromNode(node *gordfParser.Node) string {
+ switch node.ID {
+ case SPDX_NOASSERTION_CAPS, SPDX_NOASSERTION_SMALL:
+ return "NOASSERTION"
+ default:
+ return node.ID
+ }
+}
diff --git a/rdfloader/parser2v3/parse_file_test.go b/rdfloader/parser2v3/parse_file_test.go
new file mode 100644
index 0000000..f7348ef
--- /dev/null
+++ b/rdfloader/parser2v3/parse_file_test.go
@@ -0,0 +1,765 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "bufio"
+ "strings"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ rdfloader2 "github.com/spdx/gordf/rdfloader/xmlreader"
+ gordfWriter "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// content is the tags within the rdf:RDF tag
+// pads the content with the enclosing rdf:RDF tag
+func wrapIntoTemplate(content string) string {
+ header := `<rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#"
+ xmlns:doap="http://usefulinc.com/ns/doap#"
+ xmlns:j.0="http://www.w3.org/2009/pointers#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">`
+ footer := `</rdf:RDF>`
+ return header + content + footer
+}
+
+func parserFromBodyContent(content string) (*rdfParser2_3, error) {
+ rdfContent := wrapIntoTemplate(content)
+ xmlreader := rdfloader2.XMLReaderFromFileObject(bufio.NewReader(strings.NewReader(rdfContent)))
+ rootBlock, err := xmlreader.Read()
+ if err != nil {
+ return nil, err
+ }
+ parser := gordfParser.New()
+ err = parser.Parse(rootBlock)
+ if err != nil {
+ return nil, err
+ }
+ nodeToTriples := gordfWriter.GetNodeToTriples(parser.Triples)
+ rdfParser := NewParser2_3(parser, nodeToTriples)
+ return rdfParser, err
+}
+
+func Test_rdfParser2_3_getArtifactFromNode(t *testing.T) {
+ // TestCase 1: artifactOf without project URI
+ rdfParser, err := parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode := gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ artifact, err := rdfParser.getArtifactFromNode(artifactOfNode)
+ if err != nil {
+ t.Errorf("error parsing a valid artifactOf node: %v", err)
+ }
+ if artifact.Name != "Jena" {
+ t.Errorf("expected name of artifact: %s, found: %s", "Jena", artifact.Name)
+ }
+ expectedHomePage := "http://www.openjena.org/"
+ if artifact.HomePage != expectedHomePage {
+ t.Errorf("wrong artifact homepage. Expected: %s, found: %s", expectedHomePage, artifact.HomePage)
+ }
+ if artifact.URI != "" {
+ t.Errorf("wrong artifact URI. Expected: %s, found: %s", "", artifact.URI)
+ }
+
+ // TestCase 2: artifactOf with a Project URI
+ rdfParser, err = parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode = gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ artifact, err = rdfParser.getArtifactFromNode(artifactOfNode)
+ if err != nil {
+ t.Errorf("error parsing a valid artifactOf node: %v", err)
+ }
+ expectedURI := "http://subversion.apache.org/doap.rdf"
+ if artifact.URI != expectedURI {
+ t.Errorf("wrong artifact URI. Expected: %s, found: %s", expectedURI, artifact.URI)
+ }
+
+ // TestCase 3: artifactOf with unknown predicate
+ rdfParser, err = parserFromBodyContent(
+ `<spdx:File>
+ <spdx:artifactOf>
+ <doap:Project rdf:about="http://subversion.apache.org/doap.rdf">
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ <doap:invalidTag rdf:ID="invalid"/>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>`)
+ if err != nil {
+ t.Errorf("unexpected error while parsing a valid example: %v", err)
+ }
+ artifactOfNode = gordfWriter.FilterTriples(rdfParser.gordfParserObj.Triples, nil, &SPDX_ARTIFACT_OF, nil)[0].Object
+ _, err = rdfParser.getArtifactFromNode(artifactOfNode)
+ if err == nil {
+ t.Errorf("must've raised an error for an invalid predicate")
+ }
+}
+
+func Test_rdfParser2_3_getFileTypeFromUri(t *testing.T) {
+ rdfParser, _ := parserFromBodyContent(``)
+
+ // TestCase 1: Valid fileType URI:
+ fileTypeURI := "http://spdx.org/rdf/terms#fileType_source"
+ fileType, err := rdfParser.getFileTypeFromUri(fileTypeURI)
+ if err != nil {
+ t.Errorf("error in a valid example: %v", err)
+ }
+ if fileType != "source" {
+ t.Errorf("wrong fileType. expected: %s, found: %s", "source", fileType)
+ }
+
+ // TestCase 2: Invalid fileType URI format.
+ fileTypeURI = "http://spdx.org/rdf/terms#source"
+ fileType, err = rdfParser.getFileTypeFromUri(fileTypeURI)
+ if err == nil {
+ t.Error("should've raised an error for invalid fileType")
+ }
+}
+
+func Test_rdfParser2_3_setUnpackagedFiles(t *testing.T) {
+ // unpackaged files are the files which are not associated with any package
+ // file associated with a package sets parser.assocWithPackage[fileID] to true.
+ rdfParser, _ := parserFromBodyContent(``)
+ file1 := &v2_3.File{FileSPDXIdentifier: common.ElementID("file1")}
+ file2 := &v2_3.File{FileSPDXIdentifier: common.ElementID("file2")}
+ file3 := &v2_3.File{FileSPDXIdentifier: common.ElementID("file3")}
+
+ // setting files to the document as if it were to be set when it was parsed using triples.
+ rdfParser.files[file1.FileSPDXIdentifier] = file1
+ rdfParser.files[file2.FileSPDXIdentifier] = file2
+ rdfParser.files[file3.FileSPDXIdentifier] = file3
+
+ // assuming file1 is associated with a package
+ rdfParser.assocWithPackage[file1.FileSPDXIdentifier] = true
+
+ rdfParser.setUnpackagedFiles()
+
+ // after setting unpackaged files, parser.doc.Files must've file2 and file3
+ if n := len(rdfParser.doc.Files); n != 2 {
+ t.Errorf("unpackage files should've had 2 files, found %d files", n)
+ }
+
+ // checking if the unpackagedFiles contain only file2 & file3.
+ for _, file := range rdfParser.doc.Files {
+ switch string(file.FileSPDXIdentifier) {
+ case "file2", "file3":
+ continue
+ default:
+ t.Errorf("unexpected file with id %s found in unpackaged files", file.FileSPDXIdentifier)
+ }
+ }
+}
+
+func Test_setFileIdentifier(t *testing.T) {
+ file := &v2_3.File{}
+
+ // TestCase 1: valid example
+ err := setFileIdentifier("http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-129", file)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if file.FileSPDXIdentifier != "129" {
+ t.Errorf("expected %s, found: %s", "129", file.FileSPDXIdentifier)
+ }
+
+ // TestCase 2: invalid example
+ err = setFileIdentifier("http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#129", file)
+ if err == nil {
+ t.Errorf("should've raised an error for an invalid example")
+ }
+}
+
+func Test_rdfParser2_3_setFileChecksumFromNode(t *testing.T) {
+ // TestCase 1: md5 checksum
+ parser, _ := parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file := &v2_3.File{}
+ err := parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ checksumValue := "d2356e0fe1c0b85285d83c6b2ad51b5f"
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha1, should've been empty")
+ }
+ case common.SHA256:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha256, should've been empty")
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for md5. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 2: valid sha1 checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for sha1. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != "" {
+ t.Errorf("incorrectly set sha256, should've been empty")
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set md5, should've been empty")
+ }
+ }
+ }
+
+ // TestCase 3: valid sha256 checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err != nil {
+ t.Errorf("error parsing a valid checksum node")
+ }
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set sha1, should've been empty")
+ }
+ case common.SHA256:
+ if checksum.Value != checksumValue {
+ t.Errorf("wrong checksum value for sha256. Expected: %s, found: %s", checksumValue, checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != checksumValue {
+ t.Errorf("incorrectly set md5, should've been empty")
+ }
+ }
+ }
+
+ // TestCase 4: checksum node without one of the mandatory attributes
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum node")
+ }
+
+ // TestCase 5: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md43" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum node")
+ }
+
+ // TestCase 6: valid checksum algorithm which is invalid for file (like md4, md6, sha384, etc.)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha2000" />
+ <spdx:checksumValue>d2356e0fe1c0b85285d83c6b2ad51b5f</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ checksumNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_CHECKSUM_CAPITALIZED)[0].Subject
+ file = &v2_3.File{}
+ err = parser.setFileChecksumFromNode(file, checksumNode)
+ if err == nil {
+ t.Errorf("should've raised an error parsing an invalid checksum algorithm for a file")
+ }
+}
+
+func Test_rdfParser2_3_getFileFromNode(t *testing.T) {
+ // TestCase 1: file with invalid id
+ parser, _ := parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gzspdx.rdf#item177"/>
+ `)
+ fileNode := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err := parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid file ID")
+ }
+
+ // TestCase 2: invalid fileType
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#source"/>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid fileType")
+ }
+
+ // TestCase 3: invalid file checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha2000" />
+ <spdx:checksumValue>0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid checksum")
+ }
+
+ // TestCase 4: invalid license concluded
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#invalid_license" />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid license Concluded")
+ }
+
+ // TestCase 5: invalid artifactOf attribute
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:unknown_tag />
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid artifactOf predicate")
+ }
+
+ // TestCase 6: invalid file dependency
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example#CommonsLangSrc"/>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid fileDependency")
+ }
+
+ // TestCase 7: invalid annotation with unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid annotation predicate")
+ }
+
+ // TestCase 8: invalid relationship
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#dynamicLink"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Errorf("should've raised an error stating invalid relationship Type")
+ }
+
+ // TestCase 8: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:unknown />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Error("should've raised an error stating invalid predicate for a file")
+ }
+
+ // TestCase 9: invalid licenseInfoInFile.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/DC0-1.0" />
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ _, err = parser.getFileFromNode(fileNode)
+ if err == nil {
+ t.Error("should've raised an error stating invalid licenseInfoInFile for a file")
+ }
+
+ // TestCase 10: Splitting of File definition into parents of different tags mustn't create new file objects.
+ fileDefinitions := []string{
+ `<spdx:Package rdf:about="#SPDXRef-Package1">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileName>time-1.9/ChangeLog</spdx:fileName>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ </spdx:File>
+ </spdx:hasFile>
+ </spdx:Package>`,
+ `<spdx:Package rdf:about="#SPDXRef-Package2">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/rdf/terms#NOASSERTION" />
+ </spdx:File>
+ </spdx:hasFile>
+ </spdx:Package>`,
+ }
+ parser, _ = parserFromBodyContent(strings.Join(fileDefinitions, ""))
+
+ var file *v2_3.File
+ packageTypeTriples := gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)
+ for _, typeTriple := range packageTypeTriples {
+ pkg, err := parser.getPackageFromNode(typeTriple.Subject)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid package: %v", err)
+ }
+ if n := len(pkg.Files); n != 1 {
+ t.Errorf("expected package to contain exactly 1 file. Found %d files", n)
+ }
+ for _, file = range pkg.Files {
+ }
+ }
+
+ // checking if all the attributes that spanned over a several tags are set in the same variable.
+ expectedFileName := "time-1.9/ChangeLog"
+ if file.FileName != expectedFileName {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileName)
+ }
+ expectedLicenseConcluded := "NOASSERTION"
+ if file.LicenseConcluded != expectedLicenseConcluded {
+ t.Errorf("expected %s, found %s", expectedLicenseConcluded, file.LicenseConcluded)
+ }
+ expectedFileType := "source"
+ if file.FileTypes[0] != expectedFileType {
+ t.Errorf("expected %s, found %s", expectedFileType, file.FileTypes)
+ }
+ expectedLicenseInfoInFile := "NOASSERTION"
+ if file.LicenseInfoInFiles[0] != expectedLicenseInfoInFile {
+ t.Errorf("expected %s, found %s", expectedLicenseInfoInFile, file.LicenseInfoInFiles[0])
+ }
+
+ // TestCase 12: checking if recursive dependencies are resolved.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="#SPDXRef-ParentFile">
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:fileDependency>
+ <spdx:File rdf:about="#SPDXRef-ChildFile">
+ <spdx:fileDependency>
+ <spdx:File rdf:about="#SPDXRef-ParentFile">
+ <spdx:fileName>ParentFile</spdx:fileName>
+ </spdx:File>
+ </spdx:fileDependency>
+ </spdx:File>
+ </spdx:fileDependency>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ file, err = parser.getFileFromNode(fileNode)
+
+ // TestCase 11: all valid attribute and it's values.
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#SPDXRef-item177">
+ <spdx:fileName>time-1.9/ChangeLog</spdx:fileName>
+ <spdx:name/>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/rdf/terms#NOASSERTION" />
+ <spdx:licenseComments>no comments</spdx:licenseComments>
+ <spdx:copyrightText>from spdx file</spdx:copyrightText>
+ <spdx:artifactOf>
+ <doap:Project>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <doap:name>Jena</doap:name>
+ </doap:Project>
+ </spdx:artifactOf>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:noticeText rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ <spdx:fileContributor>Some Organization</spdx:fileContributor>
+ <spdx:fileDependency rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-CommonsLangSrc"/>
+ <spdx:attributionText>attribution text</spdx:attributionText>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>File level annotation copied from a spdx document</rdfs:comment>
+ <spdx:annotator>Person: File Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_contains"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Package"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ fileNode = gordfWriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0].Subject
+ file, err = parser.getFileFromNode(fileNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid file: %v", err)
+ }
+
+ // checking each and every attribute of the obtained file.
+
+ expectedFileName = "time-1.9/ChangeLog"
+ if file.FileName != expectedFileName {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileName)
+ }
+
+ if len(file.FileTypes) != 1 {
+ t.Errorf("given file should have 1 fileType attribute. found %d", len(file.FileTypes))
+ }
+ expectedFileType = "source"
+ if file.FileTypes[0] != expectedFileType {
+ t.Errorf("expected %s, found %s", expectedFileType, file.FileTypes)
+ }
+
+ expectedChecksum := "0a3a0e1ab72b7c132f5021c538a7a3ea6d539bcd"
+
+ for _, checksum := range file.Checksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != expectedChecksum {
+ t.Errorf("expected %s, found %s", expectedChecksum, checksum.Value)
+ }
+ }
+ }
+
+ expectedLicenseConcluded = "NOASSERTION"
+ if file.LicenseConcluded != expectedLicenseConcluded {
+ t.Errorf("expected %s, found %s", expectedLicenseConcluded, file.LicenseConcluded)
+ }
+
+ if len(file.LicenseInfoInFiles) != 1 {
+ t.Errorf("given file should have 1 licenseInfoInFile attribute. found %d", len(file.LicenseInfoInFiles))
+ }
+ expectedLicenseInfoInFile = "NOASSERTION"
+ if file.LicenseInfoInFiles[0] != expectedLicenseInfoInFile {
+ t.Errorf("expected %s, found %s", expectedLicenseInfoInFile, file.LicenseInfoInFiles[0])
+ }
+
+ expectedLicenseComments := "no comments"
+ if file.LicenseComments != expectedLicenseComments {
+ t.Errorf("expected %s, found %s", expectedLicenseComments, file.LicenseComments)
+ }
+
+ expectedCopyrightText := "from spdx file"
+ if file.FileCopyrightText != expectedCopyrightText {
+ t.Errorf("expected %s, found %s", expectedCopyrightText, file.FileCopyrightText)
+ }
+
+ if n := len(file.ArtifactOfProjects); n != 1 {
+ t.Errorf("given file should have 1 artifactOfProjects attribute. found %d", n)
+ }
+ artifactOf := file.ArtifactOfProjects[0]
+ expectedHomePage := "http://www.openjena.org/"
+ if artifactOf.HomePage != expectedHomePage {
+ t.Errorf("expected %s, found %s", expectedHomePage, artifactOf.HomePage)
+ }
+ if artifactOf.Name != "Jena" {
+ t.Errorf("expected %s, found %s", "Jena", artifactOf.Name)
+ }
+ if artifactOf.URI != "" {
+ t.Errorf("expected artifactOf uri to be empty, found %s", artifactOf.URI)
+ }
+
+ expectedFileComment := "no comments"
+ if file.FileComment != expectedFileComment {
+ t.Errorf("expected %s, found %s", expectedFileName, file.FileComment)
+ }
+
+ expectedNoticeText := "NOASSERTION"
+ if file.FileNotice != expectedNoticeText {
+ t.Errorf("expected %s, found %s", expectedNoticeText, file.FileNotice)
+ }
+
+ if n := len(file.FileContributors); n != 1 {
+ t.Errorf("given file should have 1 fileContributor. found %d", n)
+ }
+ expectedFileContributor := "Some Organization"
+ if file.FileContributors[0] != expectedFileContributor {
+ t.Errorf("expected %s, found %s", expectedFileContributor, file.FileContributors)
+ }
+
+ if n := len(file.FileDependencies); n != 1 {
+ t.Errorf("given file should have 1 fileDependencies. found %d", n)
+ }
+ expectedFileDependency := "CommonsLangSrc"
+ if file.FileDependencies[0] != expectedFileDependency {
+ t.Errorf("expected %s, found %s", expectedFileDependency, file.FileDependencies[0])
+ }
+
+ if n := len(file.FileAttributionTexts); n != 1 {
+ t.Errorf("given file should have 1 attributionText. found %d", n)
+ }
+ expectedAttributionText := "attribution text"
+ if file.FileAttributionTexts[0] != expectedAttributionText {
+ t.Errorf("expected %s, found %s", expectedAttributionText, file.FileAttributionTexts[0])
+ }
+
+ if n := len(parser.doc.Annotations); n != 1 {
+ t.Errorf("doc should've had 1 annotation. found %d", n)
+ }
+ ann := parser.doc.Annotations[0]
+ expectedAnnDate := "2011-01-29T18:30:22Z"
+ if ann.AnnotationDate != expectedAnnDate {
+ t.Errorf("expected %s, found %s", expectedAnnDate, ann.AnnotationDate)
+ }
+ expectedAnnComment := "File level annotation copied from a spdx document"
+ if ann.AnnotationComment != expectedAnnComment {
+ t.Errorf("expected %s, found %s", expectedAnnComment, ann.AnnotationComment)
+ }
+ expectedAnnotationType := "OTHER"
+ if ann.AnnotationType != expectedAnnotationType {
+ t.Errorf("expected %s, found %s", expectedAnnotationType, ann.AnnotationType)
+ }
+ expectedAnnotator := "File Commenter"
+ if ann.Annotator.Annotator != expectedAnnotator {
+ t.Errorf("expected %s, found %s", expectedAnnotator, ann.Annotator)
+ }
+ expectedAnnotatorType := "Person"
+ if ann.AnnotationType != expectedAnnotationType {
+ t.Errorf("expected %s, found %s", expectedAnnotatorType, ann.Annotator.AnnotatorType)
+ }
+
+ if n := len(parser.doc.Relationships); n != 1 {
+ t.Errorf("doc should've had 1 relation. found %d", n)
+ }
+ reln := parser.doc.Relationships[0]
+ expectedRefAEID := "item177"
+ if reln.RefA.DocumentRefID != "" {
+ t.Errorf("expected refA.DocumentRefID to be empty, found %s", reln.RefA.DocumentRefID)
+ }
+ if string(reln.RefA.ElementRefID) != expectedRefAEID {
+ t.Errorf("expected %s, found %s", expectedRefAEID, reln.RefA.ElementRefID)
+ }
+ expectedRefBEID := "Package"
+ if reln.RefB.DocumentRefID != "" {
+ t.Errorf("expected refB.DocumentRefID to be empty, found %s", reln.RefB.DocumentRefID)
+ }
+ if string(reln.RefB.ElementRefID) != expectedRefBEID {
+ t.Errorf("expected %s, found %s", expectedRefBEID, reln.RefB.ElementRefID)
+ }
+ expectedRelationType := "contains"
+ if reln.Relationship != expectedRelationType {
+ t.Errorf("expected %s, found %s", expectedRefBEID, reln.RefB.ElementRefID)
+ }
+ if reln.RelationshipComment != "" {
+ t.Errorf("expected relationship comment to be empty, found %s", reln.RelationshipComment)
+ }
+}
+
+func Test_getNoticeTextFromNode(t *testing.T) {
+ // TestCase 1: SPDX_NOASSERTION_SMALL must return NOASSERTION
+ output := getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_NOASSERTION_SMALL,
+ })
+ if strings.ToUpper(output) != "NOASSERTION" {
+ t.Errorf("expected NOASSERTION, found %s", strings.ToUpper(output))
+ }
+
+ // TestCase 2: SPDX_NOASSERTION_CAPS must return NOASSERTION
+ output = getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_NOASSERTION_CAPS,
+ })
+ if strings.ToUpper(output) != "NOASSERTION" {
+ t.Errorf("expected NOASSERTION, found %s", strings.ToUpper(output))
+ }
+
+ // TestCase 3: not a NOASSERTION must return the field verbatim
+ // TestCase 1: SPDX_NOASSERTION_SMALL must return NOASSERTION
+ output = getNoticeTextFromNode(&gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "text",
+ })
+ if output != "text" {
+ t.Errorf("expected text, found %s", output)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_license.go b/rdfloader/parser2v3/parse_license.go
new file mode 100644
index 0000000..b75ca05
--- /dev/null
+++ b/rdfloader/parser2v3/parse_license.go
@@ -0,0 +1,291 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+)
+
+// AnyLicense is a baseClass for all the licenses
+// All the types of licenses is a sub-type of AnyLicense,
+// either directly or indirectly.
+// This function acts as a mux for all the licenses. Based on the input, it
+// decides which type of license it is and passes control to that type of
+// license parser to parse the given input.
+func (parser *rdfParser2_3) getAnyLicenseFromNode(node *gordfParser.Node) (AnyLicenseInfo, error) {
+
+ currState := parser.cache[node.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[node.ID] = &nodeState{
+ object: nil, // not storing the object as we won't retrieve it later.
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this license node.
+ // We have a cyclic dependency!
+ return nil, errors.New("Couldn't parse license: found a cyclic dependency on " + node.ID)
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[node.ID].Color = GREY
+
+ // setting state color to black when we're done parsing this node.
+ defer func() { parser.cache[node.ID].Color = BLACK }()
+
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ if len(associatedTriples) == 0 {
+ // just a license uri string was found.
+ return parser.getSpecialLicenseFromNode(node)
+ }
+
+ // we have some attributes associated with the license node.
+ nodeType, err := getNodeTypeFromTriples(associatedTriples, node)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license triple: %v", err)
+ }
+ switch nodeType {
+ case SPDX_DISJUNCTIVE_LICENSE_SET:
+ return parser.getDisjunctiveLicenseSetFromNode(node)
+ case SPDX_CONJUNCTIVE_LICENSE_SET:
+ return parser.getConjunctiveLicenseSetFromNode(node)
+ case SPDX_EXTRACTED_LICENSING_INFO:
+ return parser.getExtractedLicensingInfoFromNode(node)
+ case SPDX_LISTED_LICENSE, SPDX_LICENSE:
+ return parser.getLicenseFromNode(node)
+ case SPDX_WITH_EXCEPTION_OPERATOR:
+ return parser.getWithExceptionOperatorFromNode(node)
+ case SPDX_OR_LATER_OPERATOR:
+ return parser.getOrLaterOperatorFromNode(node)
+ case SPDX_SIMPLE_LICENSING_INFO:
+ return parser.getSimpleLicensingInfoFromNode(node)
+ }
+ return nil, fmt.Errorf("Unknown subTag (%s) found while parsing AnyLicense", nodeType)
+}
+
+func (parser *rdfParser2_3) getLicenseExceptionFromNode(node *gordfParser.Node) (exception LicenseException, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ for _, triple := range associatedTriples {
+ value := triple.Object.ID
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_LICENSE_EXCEPTION_ID:
+ exception.licenseExceptionId = value
+ case SPDX_LICENSE_EXCEPTION_TEXT:
+ exception.licenseExceptionText = value
+ case RDFS_SEE_ALSO:
+ if !isUriValid(value) {
+ return exception, fmt.Errorf("invalid uri (%s) for seeAlso attribute of LicenseException", value)
+ }
+ exception.seeAlso = value
+ case SPDX_NAME:
+ exception.name = value
+ case SPDX_EXAMPLE:
+ exception.example = value
+ case RDFS_COMMENT:
+ exception.comment = value
+ default:
+ return exception, fmt.Errorf("invalid predicate(%s) for LicenseException", triple.Predicate)
+ }
+ }
+ return exception, nil
+}
+
+func (parser *rdfParser2_3) getSimpleLicensingInfoFromNode(node *gordfParser.Node) (SimpleLicensingInfo, error) {
+ simpleLicensingTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ return parser.getSimpleLicensingInfoFromTriples(simpleLicensingTriples)
+}
+
+func (parser *rdfParser2_3) getWithExceptionOperatorFromNode(node *gordfParser.Node) (operator WithExceptionOperator, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var memberFound bool
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ if memberFound {
+ return operator,
+ fmt.Errorf("more than one member found in the WithExceptionOperator (expected only 1)")
+ }
+ memberFound = true
+ member, err := parser.getSimpleLicensingInfoFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing member of a WithExceptionOperator: %v", err)
+ }
+ operator.member = member
+ case SPDX_LICENSE_EXCEPTION:
+ operator.licenseException, err = parser.getLicenseExceptionFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing licenseException of WithExceptionOperator: %v", err)
+ }
+ default:
+ return operator, fmt.Errorf("unknown predicate (%s) for a WithExceptionOperator", triple.Predicate.ID)
+ }
+ }
+ return operator, nil
+}
+
+func (parser *rdfParser2_3) getOrLaterOperatorFromNode(node *gordfParser.Node) (operator OrLaterOperator, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ n := len(associatedTriples)
+ if n != 2 {
+ return operator, fmt.Errorf("orLaterOperator must be associated with exactly one tag. found %v triples", n-1)
+ }
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ operator.member, err = parser.getSimpleLicensingInfoFromNode(triple.Object)
+ if err != nil {
+ return operator, fmt.Errorf("error parsing simpleLicensingInfo of OrLaterOperator: %v", err)
+ }
+ default:
+ return operator, fmt.Errorf("unknown predicate %s", triple.Predicate.ID)
+ }
+ }
+ return operator, nil
+}
+
+// SpecialLicense is a type of license which is not defined in any of the
+// spdx documents, it is a type of license defined for the sake of brevity.
+// It can be [NONE|NOASSERTION|LicenseRef-<string>]
+func (parser *rdfParser2_3) getSpecialLicenseFromNode(node *gordfParser.Node) (lic SpecialLicense, err error) {
+ uri := strings.TrimSpace(node.ID)
+ switch uri {
+ case SPDX_NONE_CAPS, SPDX_NONE_SMALL:
+ return SpecialLicense{
+ value: NONE,
+ }, nil
+ case SPDX_NOASSERTION_SMALL, SPDX_NOASSERTION_CAPS:
+ return SpecialLicense{
+ value: NOASSERTION,
+ }, nil
+ }
+
+ // the license is neither NONE nor NOASSERTION
+ // checking if the license is among the standardLicenses
+ licenseAbbreviation := getLastPartOfURI(uri)
+ for _, stdLicense := range AllStandardLicenseIDS() {
+ if licenseAbbreviation == stdLicense {
+ return SpecialLicense{
+ value: SpecialLicenseValue(stdLicense),
+ }, nil
+ }
+ }
+ return lic, fmt.Errorf("found a custom license uri (%s) without any associated fields", uri)
+}
+
+func (parser *rdfParser2_3) getDisjunctiveLicenseSetFromNode(node *gordfParser.Node) (DisjunctiveLicenseSet, error) {
+ licenseSet := DisjunctiveLicenseSet{
+ members: []AnyLicenseInfo{},
+ }
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ member, err := parser.getAnyLicenseFromNode(triple.Object)
+ if err != nil {
+ return licenseSet, fmt.Errorf("error parsing disjunctive license set: %v", err)
+ }
+ licenseSet.members = append(licenseSet.members, member)
+ }
+ }
+ return licenseSet, nil
+}
+
+func (parser *rdfParser2_3) getConjunctiveLicenseSetFromNode(node *gordfParser.Node) (ConjunctiveLicenseSet, error) {
+ licenseSet := ConjunctiveLicenseSet{
+ members: []AnyLicenseInfo{},
+ }
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_MEMBER:
+ member, err := parser.getAnyLicenseFromNode(triple.Object)
+ if err != nil {
+ return licenseSet, fmt.Errorf("error parsing conjunctive license set: %v", err)
+ }
+ licenseSet.members = append(licenseSet.members, member)
+ default:
+ return licenseSet, fmt.Errorf("unknown subTag for ConjunctiveLicenseSet: %s", triple.Predicate.ID)
+ }
+ }
+ return licenseSet, nil
+}
+
+func (parser *rdfParser2_3) getSimpleLicensingInfoFromTriples(triples []*gordfParser.Triple) (lic SimpleLicensingInfo, err error) {
+ for _, triple := range triples {
+ switch triple.Predicate.ID {
+ case RDFS_COMMENT:
+ lic.comment = triple.Object.ID
+ case SPDX_LICENSE_ID:
+ lic.licenseID = triple.Object.ID
+ case SPDX_NAME:
+ lic.name = triple.Object.ID
+ case RDFS_SEE_ALSO:
+ if !isUriValid(triple.Object.ID) {
+ return lic, fmt.Errorf("%s is not a valid uri for seeAlso attribute of a License", triple.Object.ID)
+ }
+ lic.seeAlso = append(lic.seeAlso, triple.Object.ID)
+ case SPDX_EXAMPLE:
+ lic.example = triple.Object.ID
+ case RDF_TYPE:
+ continue
+ default:
+ return lic, fmt.Errorf("unknown predicate(%s) for simple licensing info", triple.Predicate)
+ }
+ }
+ return lic, nil
+}
+
+func (parser *rdfParser2_3) getLicenseFromNode(node *gordfParser.Node) (lic License, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var restTriples []*gordfParser.Triple
+ for _, triple := range associatedTriples {
+ value := triple.Object.ID
+ switch triple.Predicate.ID {
+ case SPDX_IS_OSI_APPROVED:
+ lic.isOsiApproved, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isOsiApproved attribute of a License: %v", err)
+ }
+ case SPDX_LICENSE_TEXT:
+ lic.licenseText = value
+ case SPDX_STANDARD_LICENSE_HEADER:
+ lic.standardLicenseHeader = value
+ case SPDX_STANDARD_LICENSE_TEMPLATE:
+ lic.standardLicenseTemplate = value
+ case SPDX_STANDARD_LICENSE_HEADER_TEMPLATE:
+ lic.standardLicenseHeaderTemplate = value
+ case SPDX_IS_DEPRECATED_LICENSE_ID:
+ lic.isDeprecatedLicenseID, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isDeprecatedLicenseId attribute of a License: %v", err)
+ }
+ case SPDX_IS_FSF_LIBRE:
+ lic.isFsfLibre, err = boolFromString(value)
+ if err != nil {
+ return lic, fmt.Errorf("error parsing isFsfLibre attribute of a License: %v", err)
+ }
+ default:
+ restTriples = append(restTriples, triple)
+ }
+ }
+ lic.SimpleLicensingInfo, err = parser.getSimpleLicensingInfoFromTriples(restTriples)
+ if err != nil {
+ return lic, fmt.Errorf("error setting simple licensing information of a License: %s", err)
+ }
+ return lic, nil
+}
diff --git a/rdfloader/parser2v3/parse_license_test.go b/rdfloader/parser2v3/parse_license_test.go
new file mode 100644
index 0000000..9ff2c88
--- /dev/null
+++ b/rdfloader/parser2v3/parse_license_test.go
@@ -0,0 +1,853 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "sort"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_3_getAnyLicenseFromNode(t *testing.T) {
+ // since this function is a mux, we just have to make sure that with each
+ // type of input, it is able to redirect the request to an appropriate
+ // license getter.
+
+ // TestCase 1: input node is just a node string without any associated
+ // triple (either a NONE|NOASSERTION) because for other case,
+ // the license should've been associated with other triples
+ parser, _ := parserFromBodyContent(``)
+ inputNode := &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NONE",
+ }
+ lic, err := parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a SpecialLicense
+ switch lic.(type) {
+ case SpecialLicense:
+ default:
+ t.Errorf("expected license to be of type SpecialLicense, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 2: DisjunctiveLicenseSet:
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a DisjunctiveLicenseSet
+ switch lic.(type) {
+ case DisjunctiveLicenseSet:
+ default:
+ t.Errorf("expected license to be of type DisjunctiveLicenseSet, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 3: ConjunctiveLicenseSet:
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ConjunctiveLicenseSet
+ switch lic.(type) {
+ case ConjunctiveLicenseSet:
+ default:
+ t.Errorf("expected license to be of type ConjunctiveLicenseSet, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 4: ExtractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.dev/spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[...]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ExtractedLicensingInfo
+ switch lic.(type) {
+ case ExtractedLicensingInfo:
+ default:
+ t.Errorf("expected license to be of type ExtractedLicensingInfo, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 4: ExtractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://spdx.dev/spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[...]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a ExtractedLicensingInfo
+ switch lic.(type) {
+ case ExtractedLicensingInfo:
+ default:
+ t.Errorf("expected license to be of type ExtractedLicensingInfo, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 5: License
+ parser, _ = parserFromBodyContent(`
+ <spdx:License rdf:about="http://spdx.org/licenses/Apache-2.0">
+ <spdx:standardLicenseTemplate>&lt;&gt; Apache License Version 2.0, January 2004 http://www.apache.org/licenses/&lt;&gt;&lt;&gt; TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION&lt;&gt; &lt;&gt; Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. &lt;&gt; Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. &lt;&gt; Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. &lt;&gt; Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: &lt;&gt; You must give any other recipients of the Work or Derivative Works a copy of this License; and &lt;&gt; You must cause any modified files to carry prominent notices stating that You changed the files; and &lt;&gt; You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and &lt;&gt; If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. &lt;&gt; Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. &lt;&gt; Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. &lt;&gt; Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. &lt;&gt; Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. &lt;&gt; Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.&lt;&gt; END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright &lt;&gt; Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.&lt;&gt;</spdx:standardLicenseTemplate>
+ <rdfs:seeAlso>http://www.apache.org/licenses/LICENSE-2.0</rdfs:seeAlso>
+ <spdx:name>Apache License 2.0</spdx:name>
+ <spdx:licenseId>Apache-2.0</spdx:licenseId>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <rdfs:seeAlso>http://www.opensource.org/licenses/Apache-2.0</rdfs:seeAlso>
+ <spdx:licenseText>...</spdx:licenseText>
+ <spdx:standardLicenseHeader>...</spdx:standardLicenseHeader>
+ </spdx:License>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a License
+ switch lic.(type) {
+ case License:
+ default:
+ t.Errorf("expected license to be of type License, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 5: WithExceptionOperator
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:licenseException>
+ <spdx:LicenseException rdf:nodeID="A1">
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment></rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:WithExceptionOperator>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a WithExceptionOperator
+ switch lic.(type) {
+ case WithExceptionOperator:
+ default:
+ t.Errorf("expected license to be of type WithExceptionOperator, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 6: OrLaterOperator
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ inputNode = parser.gordfParserObj.Triples[0].Subject
+ lic, err = parser.getAnyLicenseFromNode(inputNode)
+ if err != nil {
+ t.Errorf("error parsing a valid license input: %v", err)
+ }
+ // checking if the return type is a OrLaterOperator
+ switch lic.(type) {
+ case OrLaterOperator:
+ default:
+ t.Errorf("expected license to be of type OrLaterOperator, found %v", reflect.TypeOf(lic))
+ }
+
+ // TestCase 7: checking if an unknown license raises an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:UnknownLicense>
+ <spdx:unknownTag />
+ </spdx:UnknownLicense>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getAnyLicenseFromNode(node)
+ t.Log(err)
+ if err == nil {
+ t.Errorf("should've raised an error for invalid input")
+ }
+
+ // TestCase 8: cyclic dependent license must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet rdf:about="#SPDXRef-RecursiveLicense">
+ <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ <spdx:member>
+ <spdx:ConjunctiveLicenseSet rdf:about="#SPDXRef-RecursiveLicense">
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:member rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-RecursiveLicense"/>
+ </spdx:ConjunctiveLicenseSet>
+ </spdx:member>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getAnyLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to cyclic dependent license. found %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getConjunctiveLicenseSetFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var licenseNode *gordfParser.Node
+ var license ConjunctiveLicenseSet
+
+ // TestCase 1: invalid license member
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license member, found <nil>")
+ }
+
+ // TestCase 2: invalid predicate in the licenseSet.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/CC0-1.0"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:unknownTag />
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate found")
+ }
+
+ // TestCase 3: valid example.
+ parser, _ = parserFromBodyContent(`
+ <spdx:ConjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:ConjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getConjunctiveLicenseSetFromNode(licenseNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing licenseSet: %v", err)
+ }
+ nMembers := len(license.members)
+ if nMembers != 2 {
+ t.Errorf("expected licenseSet to have 2 members, found %d", nMembers)
+ }
+ licenseMembers := mapLicensesToStrings(license.members)
+ expectedLicenseMembers := []string{"LGPL-2.0", "Nokia"}
+ sort.Strings(licenseMembers)
+ if !reflect.DeepEqual(licenseMembers, expectedLicenseMembers) {
+ t.Errorf("expected %v, found %v", expectedLicenseMembers, licenseMembers)
+ }
+}
+
+func Test_rdfParser2_3_getDisjunctiveLicenseSetFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var licenseNode *gordfParser.Node
+ var license DisjunctiveLicenseSet
+
+ // TestCase 1: invalid license member
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license member, found <nil>")
+ }
+
+ // TestCase 2: invalid predicate in the licenseSet.
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Unknown"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ <spdx:unknownTag />
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate found")
+ }
+
+ // TestCase 3: valid example.
+ parser, _ = parserFromBodyContent(`
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ `)
+ licenseNode = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getDisjunctiveLicenseSetFromNode(licenseNode)
+ if err != nil {
+ t.Errorf("unexpected error parsing licenseSet: %v", err)
+ }
+ nMembers := len(license.members)
+ if nMembers != 2 {
+ t.Errorf("expected licenseSet to have 2 members, found %d", nMembers)
+ }
+ licenseMembers := mapLicensesToStrings(license.members)
+ expectedLicenseMembers := []string{"LGPL-2.0", "Nokia"}
+ sort.Strings(licenseMembers)
+ if !reflect.DeepEqual(licenseMembers, expectedLicenseMembers) {
+ t.Errorf("expected %v, found %v", expectedLicenseMembers, licenseMembers)
+ }
+}
+
+func Test_rdfParser2_3_getLicenseExceptionFromNode(t *testing.T) {
+ var licenseException LicenseException
+ var err error
+ var node *gordfParser.Node
+ var parser *rdfParser2_3
+
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <rdfs:seeAlso>see-also</rdfs:seeAlso>
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment></rdfs:comment>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getLicenseExceptionFromNode(node)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid uri for rdfs:seeAlso")
+ }
+
+ // TestCase 2: invalid predicate for licenseException
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <spdx:example></spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:unknown></rdfs:unknown>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getLicenseExceptionFromNode(node)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid predicate")
+ }
+
+ // TestCase 3: everything valid
+ // TestCase 1: invalid value for rdf:seeAlso
+ parser, _ = parserFromBodyContent(`
+ <spdx:LicenseException>
+ <rdfs:seeAlso rdf:resource="http://www.opensource.org/licenses/GPL-3.0"/>
+ <spdx:example>no example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:licenseExceptionText>text</spdx:licenseExceptionText>
+ <spdx:name>name</spdx:name>
+ </spdx:LicenseException>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ licenseException, err = parser.getLicenseExceptionFromNode(node)
+ if err != nil {
+ t.Fatalf("unexpected error while parsing a valid licenseException")
+ }
+ expectedCrossReference := "http://www.opensource.org/licenses/GPL-3.0"
+ if licenseException.seeAlso != expectedCrossReference {
+ t.Errorf("expected: %s, found: %s", expectedCrossReference, licenseException.seeAlso)
+ }
+ expectedExample := "no example"
+ if licenseException.example != expectedExample {
+ t.Errorf("expected: %s, got: %s", expectedExample, licenseException.example)
+ }
+ if licenseException.licenseExceptionId != "Libtool-exception" {
+ t.Errorf("expected: %s, got: %s", "Libtool-exception", licenseException.licenseExceptionId)
+ }
+ if licenseException.comment != "no comments" {
+ t.Errorf("expected: %s, got: %s", "no comments", licenseException.comment)
+ }
+ if licenseException.licenseExceptionText != "text" {
+ t.Errorf("expected: '%s', got: '%s'", "text", licenseException.licenseExceptionText)
+ }
+ if licenseException.name != "name" {
+ t.Errorf("expected: '%s', got: '%s'", "name", licenseException.name)
+ }
+}
+
+func Test_rdfParser2_3_getLicenseFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var license License
+ var err error
+
+ // TestCase 1: isOsiApproved is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isOsiApproved>no</spdx:isOsiApproved>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isOsiApproved should be a valid boolean type")
+ }
+
+ // TestCase 2: rdf:seeAlso not a valid uri must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <rdfs:seeAlso>uri</rdfs:seeAlso>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating invalid uri for rdfs:seeAlso")
+ }
+
+ // TestCase 3: isDeprecatedLicenseId is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isDeprecatedLicenseId>yes</spdx:isDeprecatedLicenseId>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isDeprecatedLicenseId should be a valid boolean type")
+ }
+
+ // TestCase 4: isFsfLibre is not a valid boolean
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:isFsfLibre>no</spdx:isFsfLibre>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected function to raise an error stating isFsfLibre should be a valid boolean type")
+ }
+
+ // TestCase 5: invalid triple for License:
+ parser, _ = parserFromBodyContent(`
+ <spdx:License>
+ <spdx:unknown />
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("invalid predicate should've raised an error, got <nil>")
+ }
+
+ // TestCase 5: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-3.0-or-later">
+ <rdfs:seeAlso>http://www.opensource.org/licenses/GPL-3.0</rdfs:seeAlso>
+ <spdx:isOsiApproved>true</spdx:isOsiApproved>
+ <spdx:licenseText>GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007</spdx:licenseText>
+ <spdx:name>GNU General Public License v3.0 or later</spdx:name>
+ <spdx:standardLicenseHeaderTemplate>...</spdx:standardLicenseHeaderTemplate>
+ <spdx:licenseId>GPL-3.0-or-later</spdx:licenseId>
+ <rdfs:comment>This license was released: 29 June 2007</rdfs:comment>
+ <spdx:isFsfLibre>true</spdx:isFsfLibre>
+ <spdx:standardLicenseHeader>...</spdx:standardLicenseHeader>
+ <spdx:standardLicenseTemplate>....</spdx:standardLicenseTemplate>
+ </spdx:License>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ license, err = parser.getLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ expectedSeeAlso := "http://www.opensource.org/licenses/GPL-3.0"
+ if len(license.seeAlso) != 1 {
+ t.Fatalf("expected seeAlso to have 1 element, got %d", len(license.seeAlso))
+ }
+ if license.seeAlso[len(license.seeAlso)-1] != expectedSeeAlso {
+ t.Errorf("expected %s, got %s", expectedSeeAlso, license.seeAlso)
+ }
+ if license.isOsiApproved != true {
+ t.Errorf("expected %t, got %t", true, license.isOsiApproved)
+ }
+ expectedLicenseText := "GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007"
+ if license.licenseText != expectedLicenseText {
+ t.Errorf("expected %s, got %s", expectedSeeAlso, license.licenseText)
+ }
+ expectedName := "GNU General Public License v3.0 or later"
+ if license.name != expectedName {
+ t.Errorf("expected %s, got %s", expectedName, license.name)
+ }
+ expectedstdLicHeader := "..."
+ if license.standardLicenseHeader != expectedstdLicHeader {
+ t.Errorf("expected %s, got %s", expectedstdLicHeader, license.standardLicenseHeader)
+ }
+ expectedLicenseId := "GPL-3.0-or-later"
+ if expectedLicenseId != license.licenseID {
+ t.Errorf("expected %s, got %s", expectedLicenseId, license.licenseID)
+ }
+ expectedLicenseComment := "This license was released: 29 June 2007"
+ if expectedLicenseComment != license.comment {
+ t.Errorf("expected %s, got %s", expectedLicenseComment, license.comment)
+ }
+ expectedstdLicTemplate := "..."
+ if license.standardLicenseHeader != expectedstdLicTemplate {
+ t.Errorf("expected %s, got %s", expectedstdLicTemplate, license.standardLicenseTemplate)
+ }
+ expectedstdLicHeaderTemplate := "..."
+ if license.standardLicenseHeaderTemplate != expectedstdLicHeaderTemplate {
+ t.Errorf("expected %s, got %s", expectedstdLicHeaderTemplate, license.standardLicenseHeaderTemplate)
+ }
+ if license.isFsfLibre != true {
+ t.Errorf("expected %t, got %t", true, license.isFsfLibre)
+ }
+}
+
+func Test_rdfParser2_3_getOrLaterOperatorFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: more than one member in the OrLaterOperator tag must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to more than one members, got <nil>")
+ }
+
+ // TestCase 2: Invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:members>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:members>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 5: invalid member
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:invalidTag />
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error parsing invalid license member, got %v", err)
+ }
+
+ // TestCase 4: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:OrLaterOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ </spdx:OrLaterOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getOrLaterOperatorFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid input: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getSimpleLicensingInfoFromNode(t *testing.T) {
+ // nothing to test. The just provides an interface to call function that
+ // uses triples to render a SimpleLicensingInfo.
+ parser, _ := parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ _, err := parser.getSimpleLicensingInfoFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getSimpleLicensingInfoFromTriples(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var license SimpleLicensingInfo
+
+ // TestCase 1: invalid rdf:seeAlso attribute
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:seeAlso>an invalid uri</rdfs:seeAlso>
+ </spdx:SimpleLicensingInfo>
+ `)
+ _, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err == nil {
+ t.Error("expected an error reporting invalid uri for rdf:seeAlso, got <nil>")
+ }
+
+ // TestCase 2: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:invalidPredicate />
+ </spdx:SimpleLicensingInfo>
+ `)
+ _, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err == nil {
+ t.Error("expected an error reporting invalid predicate, got <nil>")
+ }
+
+ // TestCase 3: valid example
+ parser, _ = parserFromBodyContent(`
+ <spdx:SimpleLicensingInfo>
+ <rdfs:comment>comment</rdfs:comment>
+ <spdx:licenseId>lid</spdx:licenseId>
+ <spdx:name>name</spdx:name>
+ <rdfs:seeAlso>https://opensource.org/licenses/MPL-1.0</rdfs:seeAlso>
+ <spdx:example>example</spdx:example>
+ </spdx:SimpleLicensingInfo>
+ `)
+ license, err = parser.getSimpleLicensingInfoFromTriples(parser.gordfParserObj.Triples)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedComment := "comment"
+ expectedLicenseId := "lid"
+ expectedName := "name"
+ expectedSeeAlso := "https://opensource.org/licenses/MPL-1.0"
+ expectedExample := "example"
+ if expectedComment != license.comment {
+ t.Errorf("expected %v, got %v", expectedComment, license.comment)
+ }
+ if expectedLicenseId != license.licenseID {
+ t.Errorf("expected %v, got %v", expectedLicenseId, license.licenseID)
+ }
+ if expectedName != license.name {
+ t.Errorf("expected %v, got %v", expectedName, license.name)
+ }
+ if len(license.seeAlso) != 1 {
+ t.Fatalf("expected seeAlso to have 1 element, found %d", len(license.seeAlso))
+ }
+ if license.seeAlso[0] != expectedSeeAlso {
+ t.Errorf("expected %v, got %v", expectedSeeAlso, license.seeAlso[0])
+ }
+ if license.example != expectedExample {
+ t.Errorf("expected %v, got %v", expectedExample, license.example)
+ }
+}
+
+func Test_rdfParser2_3_getSpecialLicenseFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var license SpecialLicense
+
+ // TestCase 1: NONE
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NONE",
+ }
+ license, err := parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "NONE" {
+ t.Errorf("expected %s, got %s", "NONE", license.value)
+ }
+
+ // TestCase 2: NOASSERTION
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: NS_SPDX + "NOASSERTION",
+ }
+ license, err = parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "NOASSERTION" {
+ t.Errorf("expected %s, got %s", "NOASSERTION", license.value)
+ }
+
+ // TestCase 4: undefined standard license
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "https://opensource.org/licenses/unknown",
+ }
+ _, err = parser.getSpecialLicenseFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error saying invalid license")
+ }
+
+ // TestCase 4: valid standard license
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "https://opensource.org/licenses/MPL-1.0",
+ }
+ license, err = parser.getSpecialLicenseFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+ if license.value != "MPL-1.0" {
+ t.Errorf("expected %s, got %s", "MPL-1.0", license.value)
+ }
+}
+
+func Test_rdfParser2_3_getWithExceptionOperatorFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: more than one member in the OrLaterOperator tag must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:member>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to more than one members, got <nil>")
+ }
+
+ // TestCase 2: Invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:members>
+ <spdx:SimpleLicensingInfo>
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ </spdx:SimpleLicensingInfo>
+ </spdx:members>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 3: Invalid member
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later">
+ <spdx:unknownTag />
+ </spdx:License>
+ </spdx:member>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to error parsing a member, got <nil>")
+ }
+
+ // TestCase 4: Invalid licenseException
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:member>
+ <spdx:licenseException>
+ <spdx:LicenseException>
+ <spdx:invalidTag />
+ <spdx:example>example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err == nil {
+ t.Error("expected an error due to invalid licenseException, got <nil>")
+ }
+
+ // TestCase 5: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:WithExceptionOperator>
+ <spdx:member>
+ <spdx:License rdf:about="http://spdx.org/licenses/GPL-2.0-or-later"/>
+ </spdx:member>
+ <spdx:licenseException>
+ <spdx:LicenseException>
+ <spdx:example>example</spdx:example>
+ <spdx:licenseExceptionId>Libtool-exception</spdx:licenseExceptionId>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:LicenseException>
+ </spdx:licenseException>
+ </spdx:WithExceptionOperator>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getWithExceptionOperatorFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_other_license_info.go b/rdfloader/parser2v3/parse_other_license_info.go
new file mode 100644
index 0000000..3e28979
--- /dev/null
+++ b/rdfloader/parser2v3/parse_other_license_info.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) getExtractedLicensingInfoFromNode(node *gordfParser.Node) (lic ExtractedLicensingInfo, err error) {
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ var restTriples []*gordfParser.Triple
+ for _, triple := range associatedTriples {
+ switch triple.Predicate.ID {
+ case SPDX_EXTRACTED_TEXT:
+ lic.extractedText = triple.Object.ID
+ default:
+ restTriples = append(restTriples, triple)
+ }
+ }
+ lic.SimpleLicensingInfo, err = parser.getSimpleLicensingInfoFromTriples(restTriples)
+ if err != nil {
+ return lic, fmt.Errorf("error setting simple licensing information of extracted licensing info: %s", err)
+ }
+ return lic, nil
+}
+
+func (parser *rdfParser2_3) extractedLicenseToOtherLicense(extLicense ExtractedLicensingInfo) (othLic v2_3.OtherLicense) {
+ othLic.LicenseIdentifier = extLicense.licenseID
+ othLic.ExtractedText = extLicense.extractedText
+ othLic.LicenseComment = extLicense.comment
+ othLic.LicenseCrossReferences = extLicense.seeAlso
+ othLic.LicenseName = extLicense.name
+ return othLic
+}
diff --git a/rdfloader/parser2v3/parse_other_license_info_test.go b/rdfloader/parser2v3/parse_other_license_info_test.go
new file mode 100644
index 0000000..cb12181
--- /dev/null
+++ b/rdfloader/parser2v3/parse_other_license_info_test.go
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_3_getExtractedLicensingInfoFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseID>LicenseRef-Freeware</spdx:licenseID>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExtractedLicensingInfoFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error saying invalid predicate, got <nil>")
+ }
+
+ // TestCase 2: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExtractedLicensingInfoFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_extractedLicenseToOtherLicense(t *testing.T) {
+ // nothing to test for this function.
+ parser, _ := parserFromBodyContent(`
+ <spdx:ExtractedLicensingInfo rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#LicenseRef-Freeware">
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText><![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]></spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ `)
+ node := parser.gordfParserObj.Triples[0].Subject
+ extLicense, _ := parser.getExtractedLicensingInfoFromNode(node)
+ othLic := parser.extractedLicenseToOtherLicense(extLicense)
+
+ if othLic.LicenseIdentifier != extLicense.licenseID {
+ t.Errorf("expected %v, got %v", othLic.LicenseIdentifier, extLicense.licenseID)
+ }
+ if othLic.ExtractedText != extLicense.extractedText {
+ t.Errorf("expected %v, got %v", othLic.ExtractedText, extLicense.extractedText)
+ }
+ if othLic.LicenseComment != extLicense.comment {
+ t.Errorf("expected %v, got %v", othLic.LicenseComment, extLicense.comment)
+ }
+ if !reflect.DeepEqual(othLic.LicenseCrossReferences, extLicense.seeAlso) {
+ t.Errorf("expected %v, got %v", othLic.LicenseCrossReferences, extLicense.seeAlso)
+ }
+ if othLic.LicenseName != extLicense.name {
+ t.Errorf("expected %v, got %v", othLic.LicenseName, extLicense.name)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_package.go b/rdfloader/parser2v3/parse_package.go
new file mode 100644
index 0000000..0cdf68c
--- /dev/null
+++ b/rdfloader/parser2v3/parse_package.go
@@ -0,0 +1,380 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) getPackageFromNode(packageNode *gordfParser.Node) (pkg *v2_3.Package, err error) {
+ pkg = &v2_3.Package{} // new package which will be returned
+
+ currState := parser.cache[packageNode.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[packageNode.ID] = &nodeState{
+ object: pkg,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this package node and we needn't parse it again.
+ return currState.object.(*v2_3.Package), nil
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[packageNode.ID].Color = GREY
+
+ // setting state color to black to indicate when we're done parsing this node.
+ defer func() { parser.cache[packageNode.ID].Color = BLACK }()
+
+ // setting the SPDXIdentifier for the package.
+ eId, err := ExtractElementID(getLastPartOfURI(packageNode.ID))
+ if err != nil {
+ return nil, fmt.Errorf("error extracting elementID of a package identifier: %v", err)
+ }
+ pkg.PackageSPDXIdentifier = eId // 3.2
+
+ // check if we already have a package initialized for this ID
+ existingPackageIndex := -1
+ for ii, existingPkg := range parser.doc.Packages {
+ if existingPkg != nil && existingPkg.PackageSPDXIdentifier == eId {
+ existingPackageIndex = ii
+ pkg = existingPkg
+ break
+ }
+ }
+
+ // iterate over all the triples associated with the provided package packageNode.
+ for _, subTriple := range parser.nodeToTriples(packageNode) {
+ switch subTriple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case SPDX_NAME: // 7.1
+ // cardinality: exactly 1
+ pkg.PackageName = subTriple.Object.ID
+ case SPDX_VERSION_INFO: // 7.3
+ // cardinality: max 1
+ pkg.PackageVersion = subTriple.Object.ID
+ case SPDX_PACKAGE_FILE_NAME: // 7.4
+ // cardinality: max 1
+ pkg.PackageFileName = subTriple.Object.ID
+ case SPDX_SUPPLIER: // 7.5
+ // cardinality: max 1
+ err = setPackageSupplier(pkg, subTriple.Object.ID)
+ case SPDX_ORIGINATOR: // 7.6
+ // cardinality: max 1
+ err = setPackageOriginator(pkg, subTriple.Object.ID)
+ case SPDX_DOWNLOAD_LOCATION: // 7.7
+ // cardinality: exactly 1
+ err = setDocumentLocationFromURI(pkg, subTriple.Object.ID)
+ case SPDX_FILES_ANALYZED: // 7.8
+ // cardinality: max 1
+ err = setFilesAnalyzed(pkg, subTriple.Object.ID)
+ case SPDX_PACKAGE_VERIFICATION_CODE: // 7.9
+ // cardinality: max 1
+ err = parser.setPackageVerificationCode(pkg, subTriple.Object)
+ case SPDX_CHECKSUM: // 7.10
+ // cardinality: min 0
+ err = parser.setPackageChecksum(pkg, subTriple.Object)
+ case DOAP_HOMEPAGE: // 7.11
+ // cardinality: max 1
+ // homepage must be a valid Uri
+ if !isUriValid(subTriple.Object.ID) {
+ return nil, fmt.Errorf("invalid uri %s while parsing doap_homepage in a package", subTriple.Object.ID)
+ }
+ pkg.PackageHomePage = subTriple.Object.ID
+ case SPDX_SOURCE_INFO: // 7.12
+ // cardinality: max 1
+ pkg.PackageSourceInfo = subTriple.Object.ID
+ case SPDX_LICENSE_CONCLUDED: // 7.13
+ // cardinality: exactly 1
+ anyLicenseInfo, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ pkg.PackageLicenseConcluded = anyLicenseInfo.ToLicenseString()
+ case SPDX_LICENSE_INFO_FROM_FILES: // 7.14
+ // cardinality: min 0
+ pkg.PackageLicenseInfoFromFiles = append(pkg.PackageLicenseInfoFromFiles, getLicenseStringFromURI(subTriple.Object.ID))
+ case SPDX_LICENSE_DECLARED: // 7.15
+ // cardinality: exactly 1
+ anyLicenseInfo, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ pkg.PackageLicenseDeclared = anyLicenseInfo.ToLicenseString()
+ case SPDX_LICENSE_COMMENTS: // 7.16
+ // cardinality: max 1
+ pkg.PackageLicenseComments = subTriple.Object.ID
+ case SPDX_COPYRIGHT_TEXT: // 7.17
+ // cardinality: exactly 1
+ pkg.PackageCopyrightText = subTriple.Object.ID
+ case SPDX_SUMMARY: // 7.18
+ // cardinality: max 1
+ pkg.PackageSummary = subTriple.Object.ID
+ case SPDX_DESCRIPTION: // 7.19
+ // cardinality: max 1
+ pkg.PackageDescription = subTriple.Object.ID
+ case RDFS_COMMENT: // 7.20
+ // cardinality: max 1
+ pkg.PackageComment = subTriple.Object.ID
+ case SPDX_EXTERNAL_REF: // 7.21
+ // cardinality: min 0
+ externalDocRef, err := parser.getPackageExternalRef(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing externalRef of a package: %v", err)
+ }
+ pkg.PackageExternalReferences = append(pkg.PackageExternalReferences, externalDocRef)
+ case SPDX_HAS_FILE: // 7.22
+ // cardinality: min 0
+ file, err := parser.getFileFromNode(subTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error setting file inside a package: %v", err)
+ }
+ parser.setFileToPackage(pkg, file)
+ case SPDX_PRIMARY_PACKAGE_PURPOSE: // 7.24
+ // cardinality: exactly 1
+ pkg.PrimaryPackagePurpose = getPrimaryPackagePurpose(subTriple.Object.ID)
+ case SPDX_RELEASE_DATE: // 7.25
+ // cardinality: exactly 1
+ pkg.ReleaseDate = subTriple.Object.ID
+ case SPDX_BUILT_DATE: // 7.26
+ // cardinality: exactly 1
+ pkg.BuiltDate = subTriple.Object.ID
+ case SPDX_VALID_UNTIL_DATE: // 7.27
+ // cardinality: exactly 1
+ pkg.ValidUntilDate = subTriple.Object.ID
+ case SPDX_RELATIONSHIP:
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ case SPDX_ATTRIBUTION_TEXT:
+ // cardinality: min 0
+ pkg.PackageAttributionTexts = append(pkg.PackageAttributionTexts, subTriple.Object.ID)
+ case SPDX_ANNOTATION:
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ default:
+ return nil, fmt.Errorf("unknown predicate id %s while parsing a package", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if existingPackageIndex != -1 {
+ parser.doc.Packages[existingPackageIndex] = pkg
+ } else {
+ parser.doc.Packages = append(parser.doc.Packages, pkg)
+ }
+
+ return pkg, nil
+}
+
+// parses externalReference found in the package by the associated triple.
+func (parser *rdfParser2_3) getPackageExternalRef(node *gordfParser.Node) (externalDocRef *v2_3.PackageExternalReference, err error) {
+ externalDocRef = &v2_3.PackageExternalReference{}
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_REFERENCE_CATEGORY:
+ // cardinality: exactly 1
+ switch triple.Object.ID {
+ case SPDX_REFERENCE_CATEGORY_SECURITY:
+ externalDocRef.Category = "SECURITY"
+ case SPDX_REFERENCE_CATEGORY_PACKAGE_MANAGER:
+ externalDocRef.Category = "PACKAGE-MANAGER"
+ case SPDX_REFERENCE_CATEGORY_OTHER:
+ externalDocRef.Category = "OTHER"
+ default:
+ return nil, fmt.Errorf("unknown packageManager uri %s", triple.Predicate.ID)
+ }
+ case RDF_TYPE:
+ continue
+ case SPDX_REFERENCE_TYPE:
+ // assumes: the reference type is associated with just the uri and
+ // other associated fields are ignored.
+ // other fields include:
+ // 1. contextualExample,
+ // 2. documentation and,
+ // 3. externalReferenceSite
+ externalDocRef.RefType = triple.Object.ID
+ case SPDX_REFERENCE_LOCATOR:
+ // cardinality: exactly 1
+ externalDocRef.Locator = triple.Object.ID
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ externalDocRef.ExternalRefComment = triple.Object.ID
+ default:
+ return nil, fmt.Errorf("unknown package external reference predicate id %s", triple.Predicate.ID)
+ }
+ }
+ return
+}
+
+func getPrimaryPackagePurpose(purpose string) string {
+ value := strings.ReplaceAll(purpose, "packagePurpose_", "")
+ value = strings.ReplaceAll(value, "_", "-")
+ value = strings.ToUpper(value)
+ switch value {
+ case "APPLICATION", "FRAMEWORK", "LIBRARY", "CONTAINER", "OPERATING-SYSTEM", "DEVICE", "FIRMWARE", "SOURCE", "ARCHIVE", "FILE", "INSTALL", "OTHER":
+ return value
+ }
+ // invalid value
+ return ""
+}
+
+func (parser *rdfParser2_3) setPackageVerificationCode(pkg *v2_3.Package, node *gordfParser.Node) error {
+ if pkg.PackageVerificationCode == nil {
+ pkg.PackageVerificationCode = &common.PackageVerificationCode{}
+ }
+ for _, subTriple := range parser.nodeToTriples(node) {
+ switch subTriple.Predicate.ID {
+ case SPDX_PACKAGE_VERIFICATION_CODE_VALUE:
+ // cardinality: exactly 1
+ pkg.PackageVerificationCode.Value = subTriple.Object.ID
+ case SPDX_PACKAGE_VERIFICATION_CODE_EXCLUDED_FILE:
+ // cardinality: min 0
+ pkg.PackageVerificationCode.ExcludedFiles = append(pkg.PackageVerificationCode.ExcludedFiles, subTriple.Object.ID)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ default:
+ return fmt.Errorf("unparsed predicate %s", subTriple.Predicate.ID)
+ }
+ }
+ return nil
+}
+
+// appends the file to the package and also sets the assocWithPackage for the
+// file to indicate the file is associated with a package
+func (parser *rdfParser2_3) setFileToPackage(pkg *v2_3.Package, file *v2_3.File) {
+ if pkg.Files == nil {
+ pkg.Files = []*v2_3.File{}
+ }
+ pkg.Files = append(pkg.Files, file)
+ parser.assocWithPackage[file.FileSPDXIdentifier] = true
+}
+
+// given a supplierObject, sets the PackageSupplier attribute of the pkg.
+// Args:
+//
+// value: [NOASSERTION | [Person | Organization]: string]
+func setPackageSupplier(pkg *v2_3.Package, value string) error {
+ value = strings.TrimSpace(value)
+ supplier := &common.Supplier{}
+ if strings.ToUpper(value) == "NOASSERTION" {
+ supplier.Supplier = "NOASSERTION"
+ pkg.PackageSupplier = supplier
+ return nil
+ }
+
+ subKey, subValue, err := ExtractSubs(value, ":")
+ if err != nil {
+ return fmt.Errorf("package supplier must be of the form NOASSERTION or [Person|Organization]: string. found: %s", value)
+ }
+ switch subKey {
+ case "Person", "Organization":
+ supplier.Supplier = subValue
+ supplier.SupplierType = subKey
+ default:
+ return fmt.Errorf("unknown supplier %s", subKey)
+ }
+
+ pkg.PackageSupplier = supplier
+
+ return nil
+}
+
+// given a OriginatorObject, sets the PackageOriginator attribute of the pkg.
+// Args:
+//
+// value: [NOASSERTION | [Person | Organization]: string]
+func setPackageOriginator(pkg *v2_3.Package, value string) error {
+ value = strings.TrimSpace(value)
+ originator := &common.Originator{}
+ if strings.ToUpper(value) == "NOASSERTION" {
+ originator.Originator = "NOASSERTION"
+ pkg.PackageOriginator = originator
+ return nil
+ }
+
+ subKey, subValue, err := ExtractSubs(value, ":")
+ if err != nil {
+ return fmt.Errorf("package Originator must be of the form NOASSERTION or [Person|Organization]: string. found: %s", value)
+ }
+ switch subKey {
+ case "Person", "Organization":
+ originator.Originator = subValue
+ originator.OriginatorType = subKey
+ default:
+ return fmt.Errorf("unknown Originator %s", subKey)
+ }
+
+ pkg.PackageOriginator = originator
+
+ return nil
+}
+
+// validates the uri and sets the location if it is valid
+func setDocumentLocationFromURI(pkg *v2_3.Package, locationURI string) error {
+ switch locationURI {
+ case SPDX_NOASSERTION_CAPS, SPDX_NOASSERTION_SMALL:
+ pkg.PackageDownloadLocation = "NOASSERTION"
+ case SPDX_NONE_CAPS, SPDX_NONE_SMALL:
+ pkg.PackageDownloadLocation = "NONE"
+ default:
+ if !isUriValid(locationURI) {
+ return fmt.Errorf("%s is not a valid uri", locationURI)
+ }
+ pkg.PackageDownloadLocation = locationURI
+ }
+ return nil
+}
+
+// sets the FilesAnalyzed attribute to the given package
+// boolValue is a string of type "true" or "false"
+func setFilesAnalyzed(pkg *v2_3.Package, boolValue string) (err error) {
+ pkg.IsFilesAnalyzedTagPresent = true
+ pkg.FilesAnalyzed, err = boolFromString(boolValue)
+ return err
+}
+
+func (parser *rdfParser2_3) setPackageChecksum(pkg *v2_3.Package, node *gordfParser.Node) error {
+ checksumAlgorithm, checksumValue, err := parser.getChecksumFromNode(node)
+ if err != nil {
+ return fmt.Errorf("error getting checksum algorithm and value from %v", node)
+ }
+ if pkg.PackageChecksums == nil {
+ pkg.PackageChecksums = make([]common.Checksum, 0, 1)
+ }
+ switch checksumAlgorithm {
+ case common.SHA1,
+ common.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6,
+ common.SHA3_256,
+ common.SHA3_384,
+ common.SHA3_512,
+ common.BLAKE2b_256,
+ common.BLAKE2b_384,
+ common.BLAKE2b_512,
+ common.BLAKE3,
+ common.ADLER32:
+ pkg.PackageChecksums = append(pkg.PackageChecksums, common.Checksum{Algorithm: checksumAlgorithm, Value: checksumValue})
+ default:
+ return fmt.Errorf("unknown checksumAlgorithm %s while parsing a package", checksumAlgorithm)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_package_test.go b/rdfloader/parser2v3/parse_package_test.go
new file mode 100644
index 0000000..23be653
--- /dev/null
+++ b/rdfloader/parser2v3/parse_package_test.go
@@ -0,0 +1,788 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_setPackageSupplier(t *testing.T) {
+ var err error
+
+ // TestCase 1: no assertion must set PackageSupplierNOASSERTION field to true
+ pkg := &v2_3.Package{}
+ err = setPackageSupplier(pkg, "NOASSERTION")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("PackageSupplier must've been set to NOASSERTION")
+ }
+
+ // TestCase 2: lower-case noassertion must also set the
+ // PackageSupplierNOASSERTION to true.
+ pkg = &v2_3.Package{}
+ err = setPackageSupplier(pkg, "noassertion")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("PackageSupplier must've been set to NOASSERTION")
+ }
+
+ // TestCase 3: invalid input without colon separator. must raise an error
+ pkg = &v2_3.Package{}
+ input := "string without colon separator"
+ err = setPackageSupplier(pkg, input)
+ if err == nil {
+ t.Errorf("invalid input \"%s\" didn't raise an error", input)
+ }
+
+ // TestCase 4: Valid Person
+ pkg = &v2_3.Package{}
+ personName := "Rishabh Bhatnagar"
+ input = "Person: " + personName
+ err = setPackageSupplier(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != personName {
+ t.Errorf("PackageSupplierPerson should be %s. found %s", personName, pkg.PackageSupplier.Supplier)
+ }
+
+ // TestCase 5: Valid Organization
+ pkg = &v2_3.Package{}
+ orgName := "SPDX"
+ input = "Organization: " + orgName
+ err = setPackageSupplier(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageSupplier.Supplier != orgName {
+ t.Errorf("PackageSupplierPerson should be %s. found %s", orgName, pkg.PackageSupplier.Supplier)
+ }
+
+ // TestCase 6: Invalid EntityType
+ pkg = &v2_3.Package{}
+ input = "InvalidEntity: entity"
+ err = setPackageSupplier(pkg, input)
+ if err == nil {
+ t.Errorf("invalid entity should've raised an error")
+ }
+}
+
+func Test_setPackageOriginator(t *testing.T) {
+ var err error
+
+ // TestCase 1: no assertion must set PackageSupplierNOASSERTION field to true
+ pkg := &v2_3.Package{}
+ err = setPackageOriginator(pkg, "NOASSERTION")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("PackageOriginator must've been set to NOASSERTION")
+ }
+
+ // TestCase 2: lower-case noassertion must also set the
+ // PackageOriginatorNOASSERTION to true.
+ pkg = &v2_3.Package{}
+ err = setPackageOriginator(pkg, "noassertion")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != "NOASSERTION" {
+ t.Errorf("PackageOriginator must've been set to NOASSERTION")
+ }
+
+ // TestCase 3: invalid input without colon separator. must raise an error
+ pkg = &v2_3.Package{}
+ input := "string without colon separator"
+ err = setPackageOriginator(pkg, input)
+ if err == nil {
+ t.Errorf("invalid input \"%s\" didn't raise an error", input)
+ }
+
+ // TestCase 4: Valid Person
+ pkg = &v2_3.Package{}
+ personName := "Rishabh Bhatnagar"
+ input = "Person: " + personName
+ err = setPackageOriginator(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != personName {
+ t.Errorf("PackageOriginatorPerson should be %s. found %s", personName, pkg.PackageOriginator.Originator)
+ }
+
+ // TestCase 5: Valid Organization
+ pkg = &v2_3.Package{}
+ orgName := "SPDX"
+ input = "Organization: " + orgName
+ err = setPackageOriginator(pkg, input)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if pkg.PackageOriginator.Originator != orgName {
+ t.Errorf("PackageOriginatorOrganization should be %s. found %s", orgName, pkg.PackageOriginator.Originator)
+ }
+
+ // TestCase 6: Invalid EntityType
+ pkg = &v2_3.Package{}
+ input = "InvalidEntity: entity"
+ err = setPackageOriginator(pkg, input)
+ if err == nil {
+ t.Errorf("invalid entity should've raised an error")
+ }
+}
+
+func Test_rdfParser2_3_setPackageVerificationCode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var pkg *v2_3.Package
+ var err error
+
+ // TestCase 1: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx.PackageVerificationCode>
+ <spdx:invalidPredicate />
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg = &v2_3.Package{}
+ err = parser.setPackageVerificationCode(pkg, node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, got <nil>")
+ }
+
+ // TestCase 2: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx.PackageVerificationCode>
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg = &v2_3.Package{}
+ err = parser.setPackageVerificationCode(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedValue := "cbceb8b5689b75a584efe35587b5d41bd48820ce"
+ if pkg.PackageVerificationCode.Value != expectedValue {
+ t.Errorf("expected %v, got %v", expectedValue, pkg.PackageVerificationCode)
+ }
+ expectedExcludedFile := "./package.spdx"
+ if pkg.PackageVerificationCode.ExcludedFiles[0] != expectedExcludedFile {
+ t.Errorf("expected %v, got %v", expectedExcludedFile, pkg.PackageVerificationCode.ExcludedFiles)
+ }
+}
+
+func Test_rdfParser2_3_getPackageExternalRef(t *testing.T) {
+ var extRef *v2_3.PackageExternalReference
+ var err error
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid reference category
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_invalid"/>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid referenceCategory, got <nil>")
+ }
+
+ // TestCase 2: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:unknownPredicate />
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid referenceCategory, got <nil>")
+ }
+
+ // TestCase 3: valid example (referenceCategory_security)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef := &v2_3.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "SECURITY",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+
+ // TestCase 4: valid example (referenceCategory_packageManager)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_packageManager"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef = &v2_3.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "PACKAGE-MANAGER",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+
+ // TestCase 5: valid example (referenceCategory_other)
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_other"/>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:ExternalRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ extRef, err = parser.getPackageExternalRef(node)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid example: %v", err)
+ }
+ expectedExtRef = &v2_3.PackageExternalReference{
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ RefType: "http://spdx.org/rdf/references/cpe23Type",
+ Category: "OTHER",
+ ExternalRefComment: "comment",
+ }
+ if !reflect.DeepEqual(extRef, expectedExtRef) {
+ t.Errorf("expected: \n%+v\ngot: \n%+v", expectedExtRef, extRef)
+ }
+}
+
+func Test_rdfParser2_3_getPrimaryPackagePurpose(t *testing.T) {
+ // TestCase 1: basic purpose
+ value := getPrimaryPackagePurpose("packagePurpose_container")
+ if value != "CONTAINER" {
+ t.Errorf("expected primary package purpose to be CONTAINER. got: '%s'", value)
+ }
+
+ // TestCase 2: purpose with underscore-to-dash
+ value = getPrimaryPackagePurpose("packagePurpose_operating_system")
+ if value != "OPERATING-SYSTEM" {
+ t.Errorf("expected primary package purpose to be OPERATING-SYSTEM. got: '%s'", value)
+ }
+
+ // TestCase 3: invalid purpose
+ value = getPrimaryPackagePurpose("packagePurpose_invalid")
+ if value != "" {
+ t.Errorf("expected invalid primary package purpose to be empty. got: '%s'", value)
+ }
+}
+
+func Test_rdfParser2_3_getPackageFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid elementId
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#upload2">
+ <spdx:name>time-1.9.tar.gz</spdx:name>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(missing SPDXRef- prefix), found %v", err)
+ }
+
+ // TestCase 2: Invalid License Concluded must raise an error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/IPL-3.0"/>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid license), found %v", err)
+ }
+
+ // TestCase 2: Invalid License Declared must raise an error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/licenses/IPL-3.0"/>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid license), found %v", err)
+ }
+
+ // TestCase 3: Invalid ExternalRef
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_invalid"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid externalRef), found %v", err)
+ }
+
+ // TestCase 4: invalid file must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:hasFile>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#item8"/>
+ </spdx:hasFile>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid file), found %v", err)
+ }
+
+ // TestCase 5: invalid predicate must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:hasFiles>
+ <spdx:File rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9.tar.gz_1535120734-spdx.rdf#item8"/>
+ </spdx:hasFiles>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid predicate), found %v", err)
+ }
+
+ // TestCase 6: invalid annotation must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid annotation), found %v", err)
+ }
+
+ // TestCase 6: invalid homepage must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <doap:homepage>u r i</doap:homepage>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error(invalid homepage uri), found %v", err)
+ }
+
+ // TestCase 7: Package tag declared more than once should be parsed into a single object's definition
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ yetAnotherPkgTriple := gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: SPDX_PACKAGE_FILE_NAME,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.LITERAL,
+ ID: "packageFileName",
+ },
+ }
+ parser.nodeStringToTriples[node.String()] = append(parser.nodeStringToTriples[node.String()], &yetAnotherPkgTriple)
+ pkg, err := parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ // validating if all the attributes that spanned over two tags are included in the parsed package.
+ expectedID := "upload2"
+ if string(pkg.PackageSPDXIdentifier) != expectedID {
+ t.Errorf("expected package id: %s, got %s", expectedID, pkg.PackageSPDXIdentifier)
+ }
+ expectedPkgFileName := "packageFileName"
+ if expectedPkgFileName != pkg.PackageFileName {
+ t.Errorf("expected package file name: %s, got %s", expectedPkgFileName, pkg.PackageFileName)
+ }
+ expectedName := "Test Package"
+ if pkg.PackageName != expectedName {
+ t.Errorf("expected package name: %s, got %s", expectedPkgFileName, pkg.PackageName)
+ }
+
+ // TestCase 8: Checking if packages can handle cyclic dependencies:
+ // Simulating a smallest possible cycle: package related to itself.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes" />
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:versionInfo>1.1.1</spdx:versionInfo>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ pkg, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+ // checking if both the attributes of the packages are set.
+ expectedVersionInfo := "1.1.1"
+ expectedPackageName := "Test Package"
+ if pkg.PackageVersion != expectedVersionInfo {
+ t.Errorf("Expected %s, found %s", expectedVersionInfo, pkg.PackageVersion)
+ }
+ if pkg.PackageName != expectedPackageName {
+ t.Errorf("Expected %s, found %s", expectedPackageName, pkg.PackageName)
+ }
+
+ // TestCase 9: everything valid
+ parser, _ = parserFromBodyContent(`
+ <spdx:Package rdf:about="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2">
+ <spdx:name>Test Package</spdx:name>
+ <spdx:versionInfo>1.1.1</spdx:versionInfo>
+ <spdx:packageFileName>time-1.9.tar.gz</spdx:packageFileName>
+ <spdx:supplier>Person: Jane Doe (jane.doe@example.com)</spdx:supplier>
+ <spdx:originator>Organization: SPDX</spdx:originator>
+ <spdx:downloadLocation rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:filesAnalyzed>true</spdx:filesAnalyzed>
+ <spdx:packageVerificationCode>
+ <spdx.PackageVerificationCode>
+ <spdx:packageVerificationCodeValue>cbceb8b5689b75a584efe35587b5d41bd48820ce</spdx:packageVerificationCodeValue>
+ <spdx:packageVerificationCodeExcludedFile>./package.spdx</spdx:packageVerificationCodeExcludedFile>
+ </spdx.PackageVerificationCode>
+ </spdx:packageVerificationCode>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>75068c26abbed3ad3980685bae21d7202d288317</spdx:checksumValue>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <doap:homepage>http://www.openjena.org/</doap:homepage>
+ <spdx:sourceInfo>uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.</spdx:sourceInfo>
+ <spdx:licenseConcluded>
+ <spdx:DisjunctiveLicenseSet>
+ <spdx:member rdf:resource="http://spdx.org/licenses/Nokia"/>
+ <spdx:member rdf:resource="http://spdx.org/licenses/LGPL-2.0"/>
+ </spdx:DisjunctiveLicenseSet>
+ </spdx:licenseConcluded>
+ <spdx:licenseInfoFromFiles rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseDeclared rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:licenseComments>Other versions available for a commercial license</spdx:licenseComments>
+ <spdx:copyrightText rdf:resource="http://spdx.org/rdf/terms#noassertion" />
+ <spdx:summary> Package for Testing </spdx:summary>
+ <spdx:description> Some tags are taken from other spdx autogenerated files </spdx:description>
+ <rdfs:comment>no comments</rdfs:comment>
+ <spdx:externalRef>
+ <spdx:ExternalRef>
+ <spdx:referenceLocator>cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*</spdx:referenceLocator>
+ <spdx:referenceType>
+ <spdx:ReferenceType rdf:about="http://spdx.org/rdf/references/cpe23Type"/>
+ </spdx:referenceType>
+ <spdx:referenceCategory rdf:resource="http://spdx.org/rdf/terms#referenceCategory_security"/>
+ </spdx:ExternalRef>
+ </spdx:externalRef>
+ <spdx:hasFile rdf:resource="http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-129" />
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes" />
+ <spdx:relatedSpdxElement rdf:resource="http://anupam-VirtualBox/repo/SPDX2_time-1.9#SPDXRef-upload2" />
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:attributionText>attribution text</spdx:attributionText>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>Package level annotation</rdfs:comment>
+ <spdx:annotator>Person: Package Commenter</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:Package>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getPackageFromNode(node)
+ if err != nil {
+ t.Errorf("error parsing a valid package: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_setFileToPackage(t *testing.T) {
+ var pkg *v2_3.Package
+ var file *v2_3.File
+ var parser *rdfParser2_3
+
+ // TestCase 1: setting to a nil files attribute shouldn't panic.
+ parser, _ = parserFromBodyContent(``)
+ pkg = &v2_3.Package{}
+ file = &v2_3.File{}
+ parser.setFileToPackage(pkg, file)
+ if len(pkg.Files) != 1 {
+ t.Errorf("expected given package to have one file after setting, got %d", len(pkg.Files))
+ }
+ if parser.assocWithPackage[file.FileSPDXIdentifier] != true {
+ t.Errorf("given file should've been associated with a package, assocWithPackage is false")
+ }
+}
+
+func Test_rdfParser2_3_setPackageChecksum(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var pkg *v2_3.Package
+ var expectedChecksumValue string
+ var err error
+
+ // TestCase 1: invalid checksum algorithm
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err == nil {
+ t.Error("expected an error due to invalid checksum node, got <nil>")
+ }
+
+ // TestCase 1: valid checksum algorithm which is invalid for package
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha2000"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err == nil {
+ t.Error("expected an error due to invalid checksum for package, got <nil>")
+ }
+
+ // TestCase 2: valid checksum (sha1)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA1:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 3: valid checksum (sha256)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.SHA256:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+
+ // TestCase 4: valid checksum (md5)
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum>
+ <spdx:checksumValue>2fd4e1c67a2d28fced849ee1bb76e7391b93eb12</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5"/>
+ </spdx:Checksum>
+ `)
+ pkg = &v2_3.Package{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setPackageChecksum(pkg, node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ expectedChecksumValue = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"
+ for _, checksum := range pkg.PackageChecksums {
+ switch checksum.Algorithm {
+ case common.MD5:
+ if checksum.Value != expectedChecksumValue {
+ t.Errorf("expected %v, got: %v", expectedChecksumValue, checksum.Value)
+ }
+ }
+ }
+}
+
+func Test_setDocumentLocationFromURI(t *testing.T) {
+ var pkg *v2_3.Package
+ var expectedDocumentLocation, gotDocumentLocation string
+ var inputURI string
+ var err error
+
+ // TestCase 1: NOASSERTION
+ inputURI = SPDX_NOASSERTION_SMALL
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "NOASSERTION"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 2: NONE
+ inputURI = SPDX_NONE_CAPS
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "NONE"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 3: valid uri
+ inputURI = "https://www.gnu.org/software/texinfo/"
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ expectedDocumentLocation = "https://www.gnu.org/software/texinfo/"
+ gotDocumentLocation = pkg.PackageDownloadLocation
+ if expectedDocumentLocation != gotDocumentLocation {
+ t.Errorf("expected: %v, got: %v", expectedDocumentLocation, gotDocumentLocation)
+ }
+
+ // TestCase 3: invalid uri
+ inputURI = " "
+ pkg = &v2_3.Package{}
+ err = setDocumentLocationFromURI(pkg, inputURI)
+ if err == nil {
+ t.Fatalf("expected an error due to invalid uri, got %v", err)
+ }
+}
+
+func Test_setFilesAnalyzed(t *testing.T) {
+ var pkg *v2_3.Package
+ var err error
+
+ // TestCase 1: not a valid bool value:
+ pkg = &v2_3.Package{}
+ err = setFilesAnalyzed(pkg, "no")
+ if err == nil {
+ t.Errorf("expected an error due to invalid bool input, got %v", err)
+ }
+
+ // TestCase 2: valid input
+ pkg = &v2_3.Package{}
+ err = setFilesAnalyzed(pkg, "true")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if !pkg.IsFilesAnalyzedTagPresent {
+ t.Errorf("should've set IsFilesAnalyzedTagPresent, got: %t", pkg.IsFilesAnalyzedTagPresent)
+ }
+ if !pkg.FilesAnalyzed {
+ t.Errorf("expected: %t, got: %t", true, pkg.FilesAnalyzed)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_relationship.go b/rdfloader/parser2v3/parse_relationship.go
new file mode 100644
index 0000000..e182a45
--- /dev/null
+++ b/rdfloader/parser2v3/parse_relationship.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// parsing the relationship that exists in the rdf document.
+// Relationship is of type RefA relationType RefB.
+// parsing the relationship appends the relationship to the current document's
+// Relationships Slice.
+func (parser *rdfParser2_3) parseRelationship(triple *gordfParser.Triple) (err error) {
+ reln := v2_3.Relationship{}
+
+ reln.RefA, err = getReferenceFromURI(triple.Subject.ID)
+ if err != nil {
+ return err
+ }
+
+ currState := parser.cache[triple.Object.ID]
+ if currState == nil {
+ // there is no entry about the state of current package node.
+ // this is the first time we're seeing this node.
+ parser.cache[triple.Object.ID] = &nodeState{
+ object: reln,
+ Color: WHITE,
+ }
+ } else if currState.Color == GREY {
+ // we have already started parsing this relationship node and we needn't parse it again.
+ return nil
+ }
+
+ // setting color of the state to grey to indicate that we've started to
+ // parse this node once.
+ parser.cache[triple.Object.ID].Color = GREY
+
+ // setting state color to black to indicate when we're done parsing this node.
+ defer func() { parser.cache[triple.Object.ID].Color = BLACK }()
+
+ for _, subTriple := range parser.nodeToTriples(triple.Object) {
+ switch subTriple.Predicate.ID {
+ case SPDX_RELATIONSHIP_TYPE:
+ // cardinality: exactly 1
+ reln.Relationship, err = getRelationshipTypeFromURI(subTriple.Object.ID)
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case SPDX_RELATED_SPDX_ELEMENT:
+ // cardinality: exactly 1
+ // assumes: spdx-element is a uri
+ reln.RefB, err = getReferenceFromURI(subTriple.Object.ID)
+ if err != nil {
+ return err
+ }
+
+ relatedSpdxElementTriples := parser.nodeToTriples(subTriple.Object)
+ if len(relatedSpdxElementTriples) == 0 {
+ continue
+ }
+
+ typeTriples := rdfwriter.FilterTriples(relatedSpdxElementTriples, &subTriple.Object.ID, &RDF_TYPE, nil)
+ if len(typeTriples) != 1 {
+ return fmt.Errorf("expected %s to have exactly one rdf:type triple. found %d triples", subTriple.Object, len(typeTriples))
+ }
+ err = parser.parseRelatedElementFromTriple(&reln, typeTriples[0])
+ if err != nil {
+ return err
+ }
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ reln.RelationshipComment = subTriple.Object.ID
+ default:
+ return fmt.Errorf("unexpected predicate id: %s", subTriple.Predicate.ID)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ parser.doc.Relationships = append(parser.doc.Relationships, &reln)
+ return nil
+}
+
+func (parser *rdfParser2_3) parseRelatedElementFromTriple(reln *v2_3.Relationship, triple *gordfParser.Triple) error {
+ // iterate over relatedElement Type and check which SpdxElement it is.
+ var err error
+ switch triple.Object.ID {
+ case SPDX_FILE:
+ file, err := parser.getFileFromNode(triple.Subject)
+ if err != nil {
+ return fmt.Errorf("error setting a file: %v", err)
+ }
+ reln.RefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: file.FileSPDXIdentifier,
+ }
+
+ case SPDX_PACKAGE:
+ pkg, err := parser.getPackageFromNode(triple.Subject)
+ if err != nil {
+ return fmt.Errorf("error setting a package inside a relationship: %v", err)
+ }
+ reln.RefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: pkg.PackageSPDXIdentifier,
+ }
+
+ case SPDX_SPDX_ELEMENT:
+ // it shouldn't be associated with any other triple.
+ // it must be a uri reference.
+ reln.RefB, err = ExtractDocElementID(getLastPartOfURI(triple.Subject.ID))
+ if err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("undefined relatedElement %s found while parsing relationship", triple.Object.ID)
+ }
+ return nil
+}
+
+// references like RefA and RefB of any relationship
+func getReferenceFromURI(uri string) (common.DocElementID, error) {
+ fragment := getLastPartOfURI(uri)
+ switch strings.ToLower(strings.TrimSpace(fragment)) {
+ case "noassertion", "none":
+ return common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: common.ElementID(strings.ToUpper(fragment)),
+ }, nil
+ }
+ return ExtractDocElementID(fragment)
+}
+
+// note: relationshipType is case sensitive.
+func getRelationshipTypeFromURI(relnTypeURI string) (string, error) {
+ relnTypeURI = strings.TrimSpace(relnTypeURI)
+ lastPart := getLastPartOfURI(relnTypeURI)
+ if !strings.HasPrefix(lastPart, PREFIX_RELATIONSHIP_TYPE) {
+ return "", fmt.Errorf("relationshipType must start with %s. found %s", PREFIX_RELATIONSHIP_TYPE, lastPart)
+ }
+ lastPart = strings.TrimPrefix(lastPart, PREFIX_RELATIONSHIP_TYPE)
+
+ lastPart = strings.TrimSpace(lastPart)
+ for _, validRelationshipType := range AllRelationshipTypes() {
+ if lastPart == validRelationshipType {
+ return lastPart, nil
+ }
+ }
+ return "", fmt.Errorf("unknown relationshipType: '%s'", lastPart)
+}
diff --git a/rdfloader/parser2v3/parse_relationship_test.go b/rdfloader/parser2v3/parse_relationship_test.go
new file mode 100644
index 0000000..9c4ddff
--- /dev/null
+++ b/rdfloader/parser2v3/parse_relationship_test.go
@@ -0,0 +1,397 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_getReferenceFromURI(t *testing.T) {
+ // TestCase 1: noassertion uri
+ ref, err := getReferenceFromURI(SPDX_NOASSERTION_CAPS)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "NOASSERTION" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "NOASSERTION")
+ }
+
+ // TestCase 2: NONE uri
+ ref, err = getReferenceFromURI(SPDX_NONE_CAPS)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "NONE" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "NONE")
+ }
+
+ // TestCase 3: Valid URI
+ ref, err = getReferenceFromURI(NS_SPDX + "SPDXRef-item1")
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if ref.DocumentRefID != "" {
+ t.Errorf("reference's documentRefID should've been empty, found %s", ref.DocumentRefID)
+ }
+ if ref.ElementRefID != "item1" {
+ t.Errorf("mismatching elementRefID. Found %s, expected %s", ref.ElementRefID, "item1")
+ }
+
+ // TestCase 3: Invalid URI
+ _, err = getReferenceFromURI(NS_SPDX + "item1")
+ if err == nil {
+ t.Errorf("should've raised an error for invalid input")
+ }
+}
+
+func Test_getRelationshipTypeFromURI(t *testing.T) {
+ // TestCase 1: valid relationshipType
+ relnType := "expandedFromArchive"
+ op, err := getRelationshipTypeFromURI(NS_SPDX + "relationshipType_" + relnType)
+ if err != nil {
+ t.Errorf("error getting relationship type from a valid input")
+ }
+ if op != relnType {
+ t.Errorf("expected %s, found %s", relnType, op)
+ }
+
+ // TestCase2: invalid relationshipType
+ relnType = "invalidRelationship"
+ _, err = getRelationshipTypeFromURI(NS_SPDX + "relationshipType_" + relnType)
+ if err == nil {
+ t.Errorf("should've raised an error for an invalid input(%s)", relnType)
+ }
+}
+
+func Test_rdfParser2_3_parseRelatedElementFromTriple(t *testing.T) {
+ // TestCase 1: Package as a related element
+ parser, _ := parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln := &v2_3.Relationship{}
+ triple := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)[0]
+ err := parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 3: invalid package as a relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_PACKAGE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Package id, got %v", err)
+ }
+
+ // TestCase 4: valid File as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 5: invalid File as a relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_FILE)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error while parsing an invalid File, got %v", err)
+ }
+
+ // TestCase 6: valid SpdxElement as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_SPDX_ELEMENT)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+ expectedRefA = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB = common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "File",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+
+ // TestCase 7: invalid SpdxElement as a related element
+ parser, _ = parserFromBodyContent(`
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:SpdxElement rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-:File"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ `)
+ reln = &v2_3.Relationship{}
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &RDF_TYPE, &SPDX_SPDX_ELEMENT)[0]
+ err = parser.parseRelatedElementFromTriple(reln, triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid documentId for SpdxElement, got %v", err)
+ }
+}
+
+func Test_rdfParser2_3_parseRelationship(t *testing.T) {
+ // TestCase 1: invalid RefA
+ parser, _ := parserFromBodyContent(`
+ <spdx:File>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err := parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid RefA")
+ }
+
+ // TestCase 3: invalid RefB
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to invalid RefB")
+ }
+
+ // TestCase 3: more than one typeTriple for relatedElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <spdx:relatedSpdxElement>
+ <spdx:File/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to more than one type triples")
+ }
+
+ // TestCase 4: undefined relatedSpdxElement
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Unknown rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to unknown relatedElement, got %v", err)
+ }
+
+ // TestCase 6: relatedElement associated with more than one type
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("expected an error due to invalid relatedElement, got %v", err)
+ }
+
+ // TestCase 5: unknown predicate inside a relationship
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relatedSpdxElement>
+ <spdx:Unknown rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <spdx:unknownPredicate/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err == nil {
+ t.Errorf("should've raised an error due to unknown predicate in a relationship")
+ }
+
+ // TestCase 8: Recursive relationships mustn't raise any error:
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship rdf:about="#SPDXRef-reln">
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon">
+ <spdx:relationship>
+ <spdx:Relationship rdf:about="#SPDXRef-reln">
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File"/>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err != nil {
+ t.Errorf("error parsing a valid example")
+ }
+
+ // TestCase 7: completely valid example:
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-File">
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon"/>
+ </spdx:relatedSpdxElement>
+ <rdfs:comment>comment</rdfs:comment>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:File>
+ `)
+ triple = rdfwriter.FilterTriples(parser.gordfParserObj.Triples, nil, &SPDX_RELATIONSHIP, nil)[0]
+ err = parser.parseRelationship(triple)
+ if err != nil {
+ t.Errorf("unexpected error parsing a valid relationship: %v", err)
+ }
+ // validating parsed attributes
+ if len(parser.doc.Relationships) != 1 {
+ t.Errorf("after parsing a valid relationship, doc should've had 1 relationship, found %d", len(parser.doc.Relationships))
+ }
+ reln := parser.doc.Relationships[0]
+ expectedRelnType := "describes"
+ if reln.Relationship != expectedRelnType {
+ t.Errorf("expected %s, found %s", expectedRelnType, reln.Relationship)
+ }
+ expectedRefA := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "File",
+ }
+ if !reflect.DeepEqual(expectedRefA, reln.RefA) {
+ t.Errorf("expected %+v, found %+v", expectedRefA, reln.RefA)
+ }
+ expectedRefB := common.DocElementID{
+ DocumentRefID: "",
+ ElementRefID: "Saxon",
+ }
+ if !reflect.DeepEqual(expectedRefB, reln.RefB) {
+ t.Errorf("expected %+v, found %+v", expectedRefB, reln.RefB)
+ }
+ expectedComment := "comment"
+ if reln.RelationshipComment != expectedComment {
+ t.Errorf("expected %v, found %v", expectedComment, reln.RelationshipComment)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_review.go b/rdfloader/parser2v3/parse_review.go
new file mode 100644
index 0000000..c1c8b02
--- /dev/null
+++ b/rdfloader/parser2v3/parse_review.go
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) setReviewFromNode(reviewedNode *gordfParser.Node) error {
+ review := v2_3.Review{}
+ for _, triple := range parser.nodeToTriples(reviewedNode) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ continue
+ case RDFS_COMMENT:
+ // cardinality: max 1
+ review.ReviewComment = triple.Object.ID
+ case SPDX_REVIEW_DATE:
+ // cardinality: exactly 1
+ review.ReviewDate = triple.Object.ID
+ case SPDX_REVIEWER:
+ // cardinality: max 1
+ var err error
+ review.ReviewerType, review.Reviewer, err = ExtractSubs(triple.Object.ID, ":")
+ if err != nil {
+ return fmt.Errorf("error parsing reviewer: %v", err)
+ }
+ default:
+ return fmt.Errorf("unknown predicate %v for review triples", triple.Predicate)
+ }
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, &review)
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_review_test.go b/rdfloader/parser2v3/parse_review_test.go
new file mode 100644
index 0000000..18818aa
--- /dev/null
+++ b/rdfloader/parser2v3/parse_review_test.go
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+)
+
+func Test_rdfParser2_3_setReviewFromNode(t *testing.T) {
+ // TestCase 1: unknown predicate must raise an error
+ parser, _ := parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ <spdx:unknown />
+ </spdx:Review>
+ `)
+ reviewNode := parser.gordfParserObj.Triples[0].Subject
+ err := parser.setReviewFromNode(reviewNode)
+ if err == nil {
+ t.Errorf("unknown predicate should've elicit an error")
+ }
+
+ // TestCase 2: wrong reviewer format must raise an error
+ parser, _ = parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ `)
+ reviewNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setReviewFromNode(reviewNode)
+ if err == nil {
+ t.Errorf("incorrect should've elicit an error")
+ }
+
+ // TestCase 3: valid input
+ parser, _ = parserFromBodyContent(`
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne</spdx:reviewer>
+ </spdx:Review>
+ `)
+ reviewNode = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setReviewFromNode(reviewNode)
+ if err != nil {
+ t.Errorf("error parsing a valid node")
+ }
+ n := len(parser.doc.Reviews)
+ if n != 1 {
+ t.Errorf("expected doc to have 1 review, found %d", n)
+ }
+ review := parser.doc.Reviews[0]
+ expectedComment := "Another example reviewer."
+ if review.ReviewComment != expectedComment {
+ t.Errorf("expected: %v, found: %s", expectedComment, review.ReviewComment)
+ }
+ expectedDate := "2011-03-13T00:00:00Z"
+ if review.ReviewDate != expectedDate {
+ t.Errorf("expected %s, found %s", expectedDate, review.ReviewDate)
+ }
+ expectedReviewer := "Suzanne"
+ if review.Reviewer != expectedReviewer {
+ t.Errorf("expected %s, found %s", expectedReviewer, review.Reviewer)
+ }
+ expectedReviewerType := "Person"
+ if review.ReviewerType != expectedReviewerType {
+ t.Errorf("expected %s, found %s", expectedReviewerType, review.ReviewerType)
+ }
+}
diff --git a/rdfloader/parser2v3/parse_snippet_info.go b/rdfloader/parser2v3/parse_snippet_info.go
new file mode 100644
index 0000000..90da873
--- /dev/null
+++ b/rdfloader/parser2v3/parse_snippet_info.go
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+ "strconv"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// Snippet Information
+// Cardinality: Optional, Many
+func (parser *rdfParser2_3) getSnippetInformationFromNode2_3(node *gordfParser.Node) (si *v2_3.Snippet, err error) {
+ si = &v2_3.Snippet{}
+
+ err = setSnippetID(node.ID, si)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, siTriple := range parser.nodeToTriples(node) {
+ switch siTriple.Predicate.ID {
+ case RDF_TYPE:
+ // cardinality: exactly 1
+ case SPDX_SNIPPET_FROM_FILE:
+ // cardinality: exactly 1
+ // file which is associated with the snippet
+ _, err := parser.getFileFromNode(siTriple.Object)
+ if err != nil {
+ return nil, err
+ }
+ docElemID, err := ExtractDocElementID(getLastPartOfURI(siTriple.Object.ID))
+ si.SnippetFromFileSPDXIdentifier = docElemID.ElementRefID
+ case SPDX_RANGE:
+ // cardinality: min 1
+ err = parser.setSnippetRangeFromNode(siTriple.Object, si)
+ if err != nil {
+ return nil, err
+ }
+ case SPDX_LICENSE_INFO_IN_SNIPPET:
+ // license info in snippet can be NONE, NOASSERTION or SimpleLicensingInfo
+ // using AnyLicenseInfo because it can redirect the request and
+ // can handle NONE & NOASSERTION
+ var anyLicense AnyLicenseInfo
+ anyLicense, err = parser.getAnyLicenseFromNode(siTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license info in snippet: %v", err)
+ }
+ si.LicenseInfoInSnippet = append(si.LicenseInfoInSnippet, anyLicense.ToLicenseString())
+ case SPDX_NAME:
+ si.SnippetName = siTriple.Object.ID
+ case SPDX_COPYRIGHT_TEXT:
+ si.SnippetCopyrightText = siTriple.Object.ID
+ case SPDX_LICENSE_COMMENTS:
+ si.SnippetLicenseComments = siTriple.Object.ID
+ case RDFS_COMMENT:
+ si.SnippetComment = siTriple.Object.ID
+ case SPDX_LICENSE_CONCLUDED:
+ var anyLicense AnyLicenseInfo
+ anyLicense, err = parser.getAnyLicenseFromNode(siTriple.Object)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing license info in snippet: %v", err)
+ }
+ si.SnippetLicenseConcluded = anyLicense.ToLicenseString()
+ default:
+ return nil, fmt.Errorf("unknown predicate %v", siTriple.Predicate.ID)
+ }
+ }
+ return si, nil
+}
+
+// given is the id of the file, sets the snippet to the file in parser.
+func (parser *rdfParser2_3) setSnippetToFileWithID(snippet *v2_3.Snippet, fileID common.ElementID) error {
+ if parser.files[fileID] == nil {
+ return fmt.Errorf("snippet refers to an undefined file with ID: %s", fileID)
+ }
+
+ // initializing snippet of the files if it is not defined already
+ if parser.files[fileID].Snippets == nil {
+ parser.files[fileID].Snippets = map[common.ElementID]*v2_3.Snippet{}
+ }
+
+ // setting the snippet to the file.
+ parser.files[fileID].Snippets[snippet.SnippetSPDXIdentifier] = snippet
+
+ return nil
+}
+
+func (parser *rdfParser2_3) setSnippetRangeFromNode(node *gordfParser.Node, si *v2_3.Snippet) error {
+ // for a range object, we can have only 3 associated triples:
+ // node -> RDF_TYPE -> Object
+ // node -> startPointer -> Object
+ // node -> endPointer -> Object
+ associatedTriples := parser.nodeToTriples(node)
+ if len(associatedTriples) != 3 {
+ return fmt.Errorf("range should be associated with exactly 3 triples, got %d", len(associatedTriples))
+ }
+
+ // Triple 1: Predicate=RDF_TYPE
+ typeTriple := rdfwriter.FilterTriples(associatedTriples, &node.ID, &RDF_TYPE, nil)
+ if len(typeTriple) != 1 {
+ // we had 3 associated triples. out of which 2 is start and end pointer,
+ // if we do not have the rdf:type triple as the third one,
+ // we have either extra or undefined predicate.
+ return fmt.Errorf("every object node must be associated with exactly one rdf:type triple, found: %d", len(typeTriple))
+ }
+
+ // getting start pointer
+ startPointerTriples := rdfwriter.FilterTriples(associatedTriples, &node.ID, &PTR_START_POINTER, nil)
+ if len(startPointerTriples) != 1 {
+ return fmt.Errorf("range object must be associated with exactly 1 startPointer, got %d", len(startPointerTriples))
+ }
+ startRangeType, start, err := parser.getPointerFromNode(startPointerTriples[0].Object, si)
+ if err != nil {
+ return fmt.Errorf("error parsing startPointer: %v", err)
+ }
+
+ // getting end pointer
+ endPointerTriples := rdfwriter.FilterTriples(associatedTriples, &node.ID, &PTR_END_POINTER, nil)
+ if len(startPointerTriples) != 1 {
+ return fmt.Errorf("range object must be associated with exactly 1 endPointer, got %d", len(endPointerTriples))
+ }
+ endRangeType, end, err := parser.getPointerFromNode(endPointerTriples[0].Object, si)
+ if err != nil {
+ return fmt.Errorf("error parsing endPointer: %v", err)
+ }
+
+ // return error when start and end pointer type is not same.
+ if startRangeType != endRangeType {
+ return fmt.Errorf("start and end range type doesn't match")
+ }
+
+ si.Ranges = []common.SnippetRange{{
+ StartPointer: common.SnippetRangePointer{FileSPDXIdentifier: si.SnippetFromFileSPDXIdentifier},
+ EndPointer: common.SnippetRangePointer{FileSPDXIdentifier: si.SnippetFromFileSPDXIdentifier},
+ }}
+
+ if startRangeType == LINE_RANGE {
+ si.Ranges[0].StartPointer.LineNumber = start
+ si.Ranges[0].EndPointer.LineNumber = end
+ } else {
+ si.Ranges[0].StartPointer.Offset = start
+ si.Ranges[0].EndPointer.Offset = end
+ }
+ return nil
+}
+
+func (parser *rdfParser2_3) getPointerFromNode(node *gordfParser.Node, si *v2_3.Snippet) (rt RangeType, number int, err error) {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case RDF_TYPE:
+ case PTR_REFERENCE:
+ err = parser.parseRangeReference(triple.Object, si)
+ case PTR_OFFSET:
+ number, err = strconv.Atoi(triple.Object.ID)
+ rt = BYTE_RANGE
+ case PTR_LINE_NUMBER:
+ number, err = strconv.Atoi(triple.Object.ID)
+ rt = LINE_RANGE
+ default:
+ err = fmt.Errorf("undefined predicate (%s) for a pointer", triple.Predicate)
+ }
+ if err != nil {
+ return
+ }
+ }
+ if rt == "" {
+ err = fmt.Errorf("range type not defined for a pointer")
+ }
+ return
+}
+
+func (parser *rdfParser2_3) parseRangeReference(node *gordfParser.Node, snippet *v2_3.Snippet) error {
+ // reference is supposed to be either a resource reference to an already
+ // defined or a new file. Unfortunately, I didn't find field where this can be set in the tools-golang data model.
+ // todo: set this reference to the snippet
+ associatedTriples := rdfwriter.FilterTriples(parser.gordfParserObj.Triples, &node.ID, nil, nil)
+ if len(associatedTriples) == 0 {
+ return nil
+ }
+ _, err := parser.getFileFromNode(node)
+ if err != nil {
+ return fmt.Errorf("error parsing a new file in a reference: %v", err)
+ }
+ return nil
+}
+
+func setSnippetID(uri string, si *v2_3.Snippet) (err error) {
+ fragment := getLastPartOfURI(uri)
+ si.SnippetSPDXIdentifier, err = ExtractElementID(fragment)
+ if err != nil {
+ return fmt.Errorf("error setting snippet identifier: %v", uri)
+ }
+ return nil
+}
diff --git a/rdfloader/parser2v3/parse_snippet_info_test.go b/rdfloader/parser2v3/parse_snippet_info_test.go
new file mode 100644
index 0000000..2147586
--- /dev/null
+++ b/rdfloader/parser2v3/parse_snippet_info_test.go
@@ -0,0 +1,536 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func Test_rdfParser2_3_getSnippetInformationFromTriple2_3(t *testing.T) {
+ var err error
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+
+ // TestCase 1: invalid snippet id:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#Snippet">
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid, got %v", err)
+ }
+
+ // TestCase 2: Invalid LicenseInfoInSnippet
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/licenses/Unknown"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid licenseInfoInSnippet, got %v", err)
+ }
+
+ // TestCase 3: Invalid range.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:range>
+ <spdx:StartEndPointer>
+ <spdx:unknownTag />
+ </spdx:StartEndPointer>
+ </spdx:range>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid range, got %v", err)
+ }
+
+ // TestCase 3: invalid file in snippetFromFile
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:snippetFromFile>
+ <spdx:File rdf:resource="http://anupam-VirtualBox/spdx.rdf#item8" />
+ </spdx:snippetFromFile>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid snippetFromFile, got %v", err)
+ }
+
+ // TestCase 4: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:unknownPredicate />
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, got %v", err)
+ }
+
+ // TestCase 5: invalid license concluded:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/licenses/Unknown"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid licenseConcluded, got %v", err)
+ }
+
+ // TestCase 6: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:snippetFromFile>
+ <spdx:File rdf:about="#SPDXRef-File" />
+ </spdx:snippetFromFile>
+ <spdx:range>
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>420</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ </spdx:range>
+ <spdx:licenseInfoInSnippet rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ <spdx:name>snippet test</spdx:name>
+ <spdx:copyrightText>test</spdx:copyrightText>
+ <spdx:licenseComments>comments</spdx:licenseComments>
+ <rdfs:comment>comments</rdfs:comment>
+ <spdx:licenseConcluded rdf:resource="http://spdx.org/rdf/terms#noassertion"/>
+ </spdx:Snippet>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getSnippetInformationFromNode2_3(node)
+ if err != nil {
+ t.Fatalf("error parsing a valid example: %v", err)
+ }
+}
+
+func Test_setSnippetID(t *testing.T) {
+ // TestCase 1: invalid input (empty)
+ err := setSnippetID("", &v2_3.Snippet{})
+ if err == nil {
+ t.Errorf("should've raised an error for empty input")
+ }
+
+ // TestCase 2: valid input
+ si := &v2_3.Snippet{}
+ err = setSnippetID("http://spdx.org/spdxdocs/spdx-example#SPDXRef-Snippet", si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if si.SnippetSPDXIdentifier != "Snippet" {
+ t.Errorf("expected: %s, found: %s", "Snippet", si.SnippetSPDXIdentifier)
+ }
+}
+
+func Test_rdfParser2_3_parseRangeReference(t *testing.T) {
+ var err error
+ var node *gordfParser.Node
+ var parser *rdfParser2_3
+ var si *v2_3.Snippet
+
+ // TestCase 1: ResourceLiteral node without a new file shouldn't raise any error.
+ si = &v2_3.Snippet{}
+ parser, _ = parserFromBodyContent(``)
+ node = &gordfParser.Node{
+ NodeType: gordfParser.RESOURCELITERAL,
+ ID: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource",
+ }
+ err = parser.parseRangeReference(node, si)
+ if err != nil {
+ t.Errorf("error parsing a valid node: %v", err)
+ }
+
+ // TestCase 2: invalid file in the reference should raise an error
+ si = &v2_3.Snippet{}
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#DoapSource">
+ <spdx:fileName> test file </spdx:fileName>
+ </spdx:File>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseRangeReference(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid file in the range reference, got %v", err)
+ }
+
+ // TestCase 3: A valid reference must set the file to the files map of the parser.
+ si = &v2_3.Snippet{}
+ parser, _ = parserFromBodyContent(`
+ <spdx:File rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource">
+ <spdx:fileName> test file </spdx:fileName>
+ </spdx:File>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseRangeReference(node, si)
+ if err != nil {
+ t.Errorf("error parsing a valid input: %v", err)
+ }
+ if len(parser.files) != 1 {
+ t.Errorf("expected parser.files to have 1 file, found %d", len(parser.files))
+ }
+}
+
+func Test_rdfParser2_3_getPointerFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var si *v2_3.Snippet
+ var err error
+ var rt RangeType
+ var number int
+
+ // TestCase 1: invalid number in the offset field must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>3-10</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid offset, got %v", err)
+ }
+
+ // TestCase 2: invalid number in the lineNumber field must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>3-10</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid offset, got %v", err)
+ }
+
+ // TestCase 3: invalid predicate in the pointer field
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <spdx:invalidTag />
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>3-10</j.0:lineNumber>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid predicate, got %v", err)
+ }
+
+ // TestCase 4: No range type defined must also raise an error
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, _, err = parser.getPointerFromNode(node, si)
+ if err == nil {
+ t.Errorf("should've raised an error parsing invalid rangeType, got %v", err)
+ }
+
+ // TestCase 5: valid example
+ parser, _ = parserFromBodyContent(`
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ rt, number, err = parser.getPointerFromNode(node, si)
+ if err != nil {
+ t.Fatalf("unexpected error parsing a valid node: %v", err)
+ }
+ if rt != BYTE_RANGE {
+ t.Errorf("expected: %s, got: %s", BYTE_RANGE, rt)
+ }
+ if number != 310 {
+ t.Errorf("expected: %d, got: %d", 310, number)
+ }
+}
+
+func Test_rdfParser2_3_setSnippetRangeFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+ var si *v2_3.Snippet
+ var node *gordfParser.Node
+
+ // TestCase 1: range with less one pointer less must raise an error
+ // (end-pointer missing in the range)
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing end pointer, got %v", err)
+ }
+
+ // TestCase 2: triples with 0 or more than one type-triple
+ parser, _ = parserFromBodyContent(`
+
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ </j.0:StartEndPointer>
+
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ dummyTriple := parser.gordfParserObj.Triples[0]
+ // resetting the node to be associated with 3 triples which will have
+ // rdf:type triple either thrice or 0 times.
+ parser.nodeStringToTriples[node.String()] = []*gordfParser.Triple{
+ dummyTriple, dummyTriple, dummyTriple,
+ }
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid rdf:type triples, got %v", err)
+ }
+
+ // TestCase 3: triples with 0 startPointer
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing start pointer, got %v", err)
+ }
+
+ // TestCase 4: triples with 0 endPointer
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to missing end pointer, got %v", err)
+ }
+
+ // TestCase 5: error parsing start pointer must be propagated to the range
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>42.0</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid start pointer, got %v", err)
+ }
+
+ // TestCase 6: error parsing end pointer must be propagated to the range
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>31+0</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to invalid end pointer, got %v", err)
+ }
+
+ // TestCase 7: mismatching start and end pointer must also raise an error.
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err == nil {
+ t.Errorf("expected an error due to mismatching start and end pointers, got %v", err)
+ }
+
+ // TestCase 8: everything valid(byte_range):
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>420</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:ByteOffsetPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:offset>310</j.0:offset>
+ </j.0:ByteOffsetPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ // TestCase 9: everything valid(line_range):
+ parser, _ = parserFromBodyContent(`
+ <j.0:StartEndPointer>
+ <j.0:startPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>420</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:startPointer>
+ <j.0:endPointer>
+ <j.0:LineCharPointer>
+ <j.0:reference rdf:resource="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DoapSource"/>
+ <j.0:lineNumber>310</j.0:lineNumber>
+ </j.0:LineCharPointer>
+ </j.0:endPointer>
+ </j.0:StartEndPointer>
+ `)
+ si = &v2_3.Snippet{}
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.setSnippetRangeFromNode(node, si)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_setSnippetToFileWithID(t *testing.T) {
+ var parser *rdfParser2_3
+ var fileId common.ElementID
+ var si *v2_3.Snippet
+ var file *v2_3.File
+ var err error
+
+ // TestCase 1: file id which is not associated with any file must raise an error.
+ parser, _ = parserFromBodyContent("")
+ si = &v2_3.Snippet{}
+ err = parser.setSnippetToFileWithID(si, fileId)
+ if err == nil {
+ t.Errorf("expected an error saying undefined file")
+ }
+
+ // TestCase 2: file exists, but snippet of the file doesn't ( it mustn't raise any error )
+ fileId = common.ElementID("File1")
+ file = &v2_3.File{
+ FileSPDXIdentifier: fileId,
+ }
+ parser.files[fileId] = file
+ file.Snippets = nil // nil snippets
+ err = parser.setSnippetToFileWithID(si, fileId)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if len(file.Snippets) != 1 {
+ t.Errorf("expected file to have 1 snippet, got %d", len(file.Snippets))
+ }
+}
diff --git a/rdfloader/parser2v3/parse_spdx_document.go b/rdfloader/parser2v3/parse_spdx_document.go
new file mode 100644
index 0000000..abf89f9
--- /dev/null
+++ b/rdfloader/parser2v3/parse_spdx_document.go
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+func (parser *rdfParser2_3) parseSpdxDocumentNode(spdxDocNode *gordfParser.Node) (err error) {
+ // shorthand for document's creation info.
+ ci := parser.doc.CreationInfo
+
+ // parse the document header information (SPDXID and document namespace)
+ // the Subject.ID is of type baseURI#spdxID
+ baseUri, offset, err := ExtractSubs(spdxDocNode.ID, "#")
+ if err != nil {
+ return err
+ }
+ parser.doc.DocumentNamespace = baseUri // 2.5
+ parser.doc.SPDXIdentifier = common.ElementID(offset) // 2.3
+
+ // parse other associated triples.
+ for _, subTriple := range parser.nodeToTriples(spdxDocNode) {
+ objectValue := subTriple.Object.ID
+ switch subTriple.Predicate.ID {
+ case RDF_TYPE:
+ continue
+ case SPDX_SPEC_VERSION: // 2.1: specVersion
+ // cardinality: exactly 1
+ parser.doc.SPDXVersion = objectValue
+ case SPDX_DATA_LICENSE: // 2.2: dataLicense
+ // cardinality: exactly 1
+ dataLicense, err := parser.getAnyLicenseFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.DataLicense = dataLicense.ToLicenseString()
+ case SPDX_NAME: // 2.4: DocumentName
+ // cardinality: exactly 1
+ parser.doc.DocumentName = objectValue
+ case SPDX_EXTERNAL_DOCUMENT_REF: // 2.6: externalDocumentReferences
+ // cardinality: min 0
+ var extRef v2_3.ExternalDocumentRef
+ extRef, err = parser.getExternalDocumentRefFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.ExternalDocumentReferences = append(parser.doc.ExternalDocumentReferences, extRef)
+ case SPDX_CREATION_INFO: // 2.7 - 2.10:
+ // cardinality: exactly 1
+ err = parser.parseCreationInfoFromNode(ci, subTriple.Object)
+ case RDFS_COMMENT: // 2.11: Document Comment
+ // cardinality: max 1
+ parser.doc.DocumentComment = objectValue
+ case SPDX_REVIEWED: // reviewed:
+ // cardinality: min 0
+ err = parser.setReviewFromNode(subTriple.Object)
+ case SPDX_DESCRIBES_PACKAGE: // describes Package
+ // cardinality: min 0
+ var pkg *v2_3.Package
+ pkg, err = parser.getPackageFromNode(subTriple.Object)
+ if err != nil {
+ return err
+ }
+ parser.doc.Packages = append(parser.doc.Packages, pkg)
+ case SPDX_HAS_EXTRACTED_LICENSING_INFO: // hasExtractedLicensingInfo
+ // cardinality: min 0
+ extractedLicensingInfo, err := parser.getExtractedLicensingInfoFromNode(subTriple.Object)
+ if err != nil {
+ return fmt.Errorf("error setting extractedLicensingInfo in spdxDocument: %v", err)
+ }
+ othLicense := parser.extractedLicenseToOtherLicense(extractedLicensingInfo)
+ parser.doc.OtherLicenses = append(parser.doc.OtherLicenses, &othLicense)
+ case SPDX_RELATIONSHIP: // relationship
+ // cardinality: min 0
+ err = parser.parseRelationship(subTriple)
+ case SPDX_ANNOTATION: // annotations
+ // cardinality: min 0
+ err = parser.parseAnnotationFromNode(subTriple.Object)
+ default:
+ return fmt.Errorf("invalid predicate while parsing SpdxDocument: %v", subTriple.Predicate)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (parser *rdfParser2_3) getExternalDocumentRefFromNode(node *gordfParser.Node) (edr v2_3.ExternalDocumentRef, err error) {
+ for _, triple := range parser.nodeToTriples(node) {
+ switch triple.Predicate.ID {
+ case SPDX_EXTERNAL_DOCUMENT_ID:
+ // cardinality: exactly 1
+ edr.DocumentRefID = triple.Object.ID
+ case SPDX_SPDX_DOCUMENT:
+ // cardinality: exactly 1
+ // assumption: "spdxDocument" property of an external document
+ // reference is just a uri which doesn't follow a spdxDocument definition
+ edr.URI = triple.Object.ID
+ case SPDX_CHECKSUM:
+ // cardinality: exactly 1
+ alg, checksum, err := parser.getChecksumFromNode(triple.Object)
+ if err != nil {
+ return edr, err
+ }
+ edr.Checksum.Value = checksum
+ edr.Checksum.Algorithm = alg
+ case RDF_TYPE:
+ continue
+ default:
+ return edr, fmt.Errorf("unknown predicate ID (%s) while parsing externalDocumentReference", triple.Predicate.ID)
+ }
+ }
+ return edr, nil
+}
diff --git a/rdfloader/parser2v3/parse_spdx_document_test.go b/rdfloader/parser2v3/parse_spdx_document_test.go
new file mode 100644
index 0000000..f2c0f2f
--- /dev/null
+++ b/rdfloader/parser2v3/parse_spdx_document_test.go
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+)
+
+func Test_rdfParser2_3_getExternalDocumentRefFromNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid checksum
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid checksum, found %v", err)
+ }
+
+ // TestCase 2: unknown predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:unknownTag />
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid predicate, found %v", err)
+ }
+
+ // TestCase 3: valid example
+ parser, _ = parserFromBodyContent(`
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha256"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ _, err = parser.getExternalDocumentRefFromNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_parseSpdxDocumentNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var node *gordfParser.Node
+ var err error
+
+ // TestCase 1: invalid spdx id of the document
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301/Document"/>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid document id, got %v", err)
+ }
+
+ // TestCase 2: erroneous dataLicense
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:dataLicense rdf:resource="http://spdx.org/rdf/terms#Unknown" />
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid dataLicense, got %v", err)
+ }
+
+ // TestCase 3: invalid external document ref
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha999"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid externalDocumentRef, got %v", err)
+ }
+
+ // TestCase 4: invalid package
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:describesPackage>
+ <spdx:Package rdf:about="http://www.spdx.org/spdxdocs/8f141b09-1138-4fc5-aecb-fc10d9ac1eed#SPDX-1"/>
+ </spdx:describesPackage>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid externalDocumentRef, got %v", err)
+ }
+
+ // TestCase 5: error in extractedLicensingInfo
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:hasExtractedLicensingInfo>
+ <spdx:ExtractedLicensingInfo rdf:about="#LicenseRef-Freeware">
+ <spdx:invalidTag />
+ <spdx:licenseId>LicenseRef-Freeware</spdx:licenseId>
+ <spdx:name>freeware</spdx:name>
+ <spdx:extractedText>
+<![CDATA[Software classified as freeware is licensed at no cost and is either fully functional for an unlimited time; or has only basic functions enabled with a fully functional version available commercially or as shareware.[8] In contrast to free software, the author usually restricts one or more rights of the user, including the rights to use, copy, distribute, modify and make derivative works of the software or extract the source code.[1][2][9][10] The software license may impose various additional restrictions on the type of use, e.g. only for personal use, private use, individual use, non-profit use, non-commercial use, academic use, educational use, use in charity or humanitarian organizations, non-military use, use by public authorities or various other combinations of these type of restrictions.[11] For instance, the license may be "free for private, non-commercial use". The software license may also impose various other restrictions, such as restricted use over a network, restricted use on a server, restricted use in a combination with some types of other software or with some hardware devices, prohibited distribution over the Internet other than linking to author's website, restricted distribution without author's consent, restricted number of copies, etc.]]>
+ </spdx:extractedText>
+ </spdx:ExtractedLicensingInfo>
+ </spdx:hasExtractedLicensingInfo>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid extractedLicensingInfo, got %v", err)
+ }
+
+ // TestCase 6: error in annotation
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:unknownAttribute />
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to invalid extractedLicensingInfo, got %v", err)
+ }
+
+ // TestCase 7: invalid predicate
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:unknownTag />
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err == nil {
+ t.Errorf("expected an error due to unknown predicate, got %v", err)
+ }
+
+ // TestCase 7: everything valid
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document">
+ <spdx:specVersion>SPDX-2.1</spdx:specVersion>
+ <spdx:dataLicense rdf:resource="http://spdx.org/licenses/CC0-1.0" />
+ <spdx:name>/test/example</spdx:name>
+ <spdx:externalDocumentRef>
+ <spdx:ExternalDocumentRef>
+ <spdx:externalDocumentId>DocumentRef-spdx-tool-1.2</spdx:externalDocumentId>
+ <spdx:checksum>
+ <spdx:Checksum>
+ <spdx:checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</spdx:checksumValue>
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+ </spdx:Checksum>
+ </spdx:checksum>
+ <spdx:spdxDocument rdf:resource="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
+ </spdx:ExternalDocumentRef>
+ </spdx:externalDocumentRef>
+ <spdx:creationInfo>
+ <spdx:CreationInfo>
+ <spdx:licenseListVersion>2.6</spdx:licenseListVersion>
+ <spdx:creator>Person: spdx (y)</spdx:creator>
+ <spdx:creator>Organization: </spdx:creator>
+ <spdx:creator>Tool: spdx2</spdx:creator>
+ <spdx:created>2018-08-24T19:55:34Z</spdx:created>
+ </spdx:CreationInfo>
+ </spdx:creationInfo>
+ <rdfs:comment>test</rdfs:comment>
+ <spdx:reviewed>
+ <spdx:Review>
+ <rdfs:comment>Another example reviewer.</rdfs:comment>
+ <spdx:reviewDate>2011-03-13T00:00:00Z</spdx:reviewDate>
+ <spdx:reviewer>Person: Suzanne Reviewer</spdx:reviewer>
+ </spdx:Review>
+ </spdx:reviewed>
+ <spdx:describesPackage>
+ <spdx:Package rdf:about="#SPDXRef-1"/>
+ </spdx:describesPackage>
+ <spdx:hasExtractedLicensingInfo rdf:resource="http://spdx.org/licenses/CC0-1.0"/>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_containedBy"/>
+ <spdx:relatedSpdxElement rdf:resource="http://spdx.org/documents/spdx-toolsv2.1.7-SNAPSHOT#SPDXRef-1"/>
+ <rdfs:comment></rdfs:comment>
+ </spdx:Relationship>
+ </spdx:relationship>
+ <spdx:annotation>
+ <spdx:Annotation>
+ <spdx:annotationDate>2011-01-29T18:30:22Z</spdx:annotationDate>
+ <rdfs:comment>test annotation</rdfs:comment>
+ <spdx:annotator>Person: Rishabh Bhatnagar</spdx:annotator>
+ <spdx:annotationType rdf:resource="http://spdx.org/rdf/terms#annotationType_other"/>
+ </spdx:Annotation>
+ </spdx:annotation>
+ </spdx:SpdxDocument>
+ `)
+ node = parser.gordfParserObj.Triples[0].Subject
+ err = parser.parseSpdxDocumentNode(node)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v3/parser.go b/rdfloader/parser2v3/parser.go
new file mode 100644
index 0000000..961af31
--- /dev/null
+++ b/rdfloader/parser2v3/parser.go
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ gordfWriter "github.com/spdx/gordf/rdfwriter"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// returns a new instance of rdfParser2_3 given the gordf object and nodeToTriples mapping
+func NewParser2_3(gordfParserObj *gordfParser.Parser, nodeToTriples map[string][]*gordfParser.Triple) *rdfParser2_3 {
+ parser := rdfParser2_3{
+ gordfParserObj: gordfParserObj,
+ nodeStringToTriples: nodeToTriples,
+ doc: &v2_3.Document{
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{},
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{},
+ Files: []*v2_3.File{},
+ OtherLicenses: []*v2_3.OtherLicense{},
+ Relationships: []*v2_3.Relationship{},
+ Annotations: []*v2_3.Annotation{},
+ Reviews: []*v2_3.Review{},
+ },
+ files: map[common.ElementID]*v2_3.File{},
+ assocWithPackage: map[common.ElementID]bool{},
+ cache: map[string]*nodeState{},
+ }
+ return &parser
+}
+
+// main function which takes in a gordfParser and returns
+// a spdxDocument model or the error encountered while parsing it
+func LoadFromGoRDFParser(gordfParserObj *gordfParser.Parser) (*v2_3.Document, error) {
+ // nodeToTriples is a mapping from a node to list of triples.
+ // for every node in the set of subjects of all the triples,
+ // it provides a list of triples that are associated with that subject node.
+ nodeToTriples := gordfWriter.GetNodeToTriples(gordfParserObj.Triples)
+ parser := NewParser2_3(gordfParserObj, nodeToTriples)
+
+ spdxDocumentNode, err := parser.getSpdxDocNode()
+ if err != nil {
+ return nil, err
+ }
+
+ err = parser.parseSpdxDocumentNode(spdxDocumentNode)
+ if err != nil {
+ return nil, err
+ }
+
+ // parsing other root elements
+ for _, rootNode := range gordfWriter.GetRootNodes(parser.gordfParserObj.Triples) {
+ typeTriples := gordfWriter.FilterTriples(gordfParserObj.Triples, &rootNode.ID, &RDF_TYPE, nil)
+ if len(typeTriples) != 1 {
+ return nil, fmt.Errorf("every node must be associated with exactly 1 type Triple. found %d type triples", len(typeTriples))
+ }
+ switch typeTriples[0].Object.ID {
+ case SPDX_SPDX_DOCUMENT_CAPITALIZED:
+ continue // it is already parsed.
+ case SPDX_SNIPPET:
+ snippet, err := parser.getSnippetInformationFromNode2_3(typeTriples[0].Subject)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing a snippet: %v", err)
+ }
+ err = parser.setSnippetToFileWithID(snippet, snippet.SnippetFromFileSPDXIdentifier)
+ if err != nil {
+ return nil, err
+ }
+ // todo: check other root node attributes.
+ default:
+ continue
+ // because in rdf it is quite possible that the root node is an
+ // element that has been used in the some other element as a child
+ }
+ }
+
+ // parsing packages and files sets the files to a files variable which is
+ // associated with the parser and not the document. following method is
+ // necessary to transfer the files which are not set in the packages to the
+ // Files attribute of the document
+ // WARNING: do not relocate following function call. It must be at the end of the function
+ parser.setUnpackagedFiles()
+ return parser.doc, nil
+}
+
+// from the given parser object, returns the SpdxDocument Node defined in the root elements.
+// returns error if the document is associated with no SpdxDocument or
+// associated with more than one SpdxDocument node.
+func (parser *rdfParser2_3) getSpdxDocNode() (node *gordfParser.Node, err error) {
+ /* Possible Questions:
+ 1. why are you traversing the root nodes only? why not directly filter out
+ all the triples with rdf:type=spdx:SpdxDocument?
+ Ans: It is quite possible that the relatedElement or any other attribute
+ to have dependency of another SpdxDocument. In that case, that
+ element will reference the dependency using SpdxDocument tag which will
+ cause false positives when direct filtering is done.
+ */
+ // iterate over root nodes and find the node which has a property of rdf:type=spdx:SpdxDocument
+ var spdxDocNode *gordfParser.Node
+ for _, rootNode := range gordfWriter.GetRootNodes(parser.gordfParserObj.Triples) {
+ typeTriples := gordfWriter.FilterTriples(
+ parser.nodeToTriples(rootNode), // triples
+ &rootNode.ID, // Subject
+ &RDF_TYPE, // Predicate
+ nil, // Object
+ )
+
+ if typeTriples[0].Object.ID == SPDX_SPDX_DOCUMENT_CAPITALIZED {
+ // we found a SpdxDocument Node
+
+ // must be associated with exactly one rdf:type.
+ if len(typeTriples) != 1 {
+ return nil, fmt.Errorf("rootNode (%v) must be associated with exactly one"+
+ " triple of predicate rdf:type, found %d triples", rootNode, len(typeTriples))
+ }
+
+ // checking if we've already found a node and it is not same as the current one.
+ if spdxDocNode != nil && spdxDocNode.ID != typeTriples[0].Subject.ID {
+ return nil, fmt.Errorf("found more than one SpdxDocument Node (%v and %v)", spdxDocNode, typeTriples[0].Subject)
+ }
+ spdxDocNode = typeTriples[0].Subject
+ }
+ }
+ if spdxDocNode == nil {
+ return nil, errors.New("RDF files must be associated with a SpdxDocument tag. No tag found")
+ }
+ return spdxDocNode, nil
+}
diff --git a/rdfloader/parser2v3/parser_test.go b/rdfloader/parser2v3/parser_test.go
new file mode 100644
index 0000000..5aebe94
--- /dev/null
+++ b/rdfloader/parser2v3/parser_test.go
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "testing"
+)
+
+func TestNewParser2_3(t *testing.T) {
+ // testing if the attributes are initialised well and no top-level is left uninitialized.
+ // primarily, checking if all the maps are initialized because
+ // uninitialized slices are by default slices of length 0
+ p, _ := parserFromBodyContent(``)
+ parser := NewParser2_3(p.gordfParserObj, p.nodeStringToTriples)
+ if parser.files == nil {
+ t.Errorf("files should've been initialised, got %v", parser.files)
+ }
+ if parser.assocWithPackage == nil {
+ t.Errorf("assocWithPackage should've been initialised, got %v", parser.assocWithPackage)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo should've been initialised, got %v", parser.doc.CreationInfo)
+ }
+ if parser.doc.Packages == nil {
+ t.Errorf("doc.Packages should've been initialised, got %v", parser.doc.Packages)
+ }
+ if parser.doc.Files == nil {
+ t.Errorf("doc.Files should've been initialised, got %v", parser.doc.Files)
+ }
+}
+
+func TestLoadFromGoRDFParser(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+
+ // TestCase 1: gordfparser without a SpdxDocument node triple:
+ parser, _ = parserFromBodyContent("")
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error because of absence of SpdxDocument node, got %v", err)
+ }
+
+ // TestCase 2: invalid SpdxDocumentNode
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301/Document">
+ <spdx:invalidTag />
+ </spdx:SpdxDocument>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error because of absence of SpdxDocument node, got %v", err)
+ }
+
+ // TestCase 3: >1 type triples for subnode of a SpdxDocument:
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#Snippet"/>
+ <spdx:CreationInfo rdf:about="#Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to more than one type triples, got %v", err)
+ }
+
+ // TestCase 4: invalid snippet must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Snippet, got %v", err)
+ }
+
+ // TestCase 5: invalid snippet not associated with any File must raise an error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet"/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err == nil {
+ t.Errorf("expected an error due to invalid Snippet File, got %v", err)
+ }
+
+ // TestCase 6: other Tag alongwith the SpdxDocument node mustn't raise any error.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:review/>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+
+ // TestCase 5: everything valid:
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Snippet">
+ <spdx:name>from linux kernel</spdx:name>
+ <spdx:copyrightText>Copyright 2008-2010 John Smith</spdx:copyrightText>
+ <spdx:licenseComments>The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.</spdx:licenseComments>
+ <spdx:snippetFromFile>
+ <spdx:File rdf:about="#SPDXRef-DoapSource">
+ <spdx:copyrightText>Copyright 2010, 2011 Source Auditor Inc.</spdx:copyrightText>
+ <spdx:fileContributor>Open Logic Inc.</spdx:fileContributor>
+ <spdx:fileName>./src/org/spdx/parser/DOAPProject.java</spdx:fileName>
+ <spdx:fileContributor>Black Duck Software In.c</spdx:fileContributor>
+ <spdx:fileType rdf:resource="http://spdx.org/rdf/terms#fileType_source"/>
+ <spdx:licenseInfoInFile rdf:resource="http://spdx.org/licenses/Apache-2.0"/>
+ </spdx:File>
+ </spdx:snippetFromFile>
+ </spdx:Snippet>
+ `)
+ _, err = LoadFromGoRDFParser(parser.gordfParserObj)
+ if err != nil {
+ t.Errorf("error parsing a valid example: %v", err)
+ }
+}
+
+func Test_rdfParser2_3_getSpdxDocNode(t *testing.T) {
+ var parser *rdfParser2_3
+ var err error
+
+ // TestCase 1: more than one association type for a single node.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Document"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ t.Log(err)
+ if err == nil {
+ t.Errorf("expected and error due to more than one type triples for the SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 2: must be associated with exactly one rdf:type.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document"/>
+ <spdx:Snippet rdf:about="#SPDXRef-Document"/>
+ <spdx:File rdf:about="#SPDXRef-DoapSource"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ t.Log(err)
+ if err == nil {
+ t.Errorf("rootNode must be associated with exactly one triple of predicate rdf:type, got %v", err)
+ }
+
+ // TestCase 3: two different spdx nodes found in a single document.
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-1"/>
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-2"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ if err == nil {
+ t.Errorf("expected and error due to more than one type SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 4: no spdx document
+ parser, _ = parserFromBodyContent(``)
+ _, err = parser.getSpdxDocNode()
+ if err == nil {
+ t.Errorf("expected and error due to no SpdxDocument Node, got %v", err)
+ }
+
+ // TestCase 5: valid spdxDocument node
+ parser, _ = parserFromBodyContent(`
+ <spdx:SpdxDocument rdf:about="#SPDXRef-Document-1"/>
+ `)
+ _, err = parser.getSpdxDocNode()
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
diff --git a/rdfloader/parser2v3/types.go b/rdfloader/parser2v3/types.go
new file mode 100644
index 0000000..9efd04e
--- /dev/null
+++ b/rdfloader/parser2v3/types.go
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+// copied from tvloader/parser2v3/types.go
+package parser2v3
+
+import (
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+type rdfParser2_3 struct {
+ // fields associated with gordf project which
+ // will be required by rdfloader
+ gordfParserObj *gordfParser.Parser
+ nodeStringToTriples map[string][]*gordfParser.Triple
+
+ // document into which data is being parsed
+ doc *v2_3.Document
+
+ // map of packages and files.
+ files map[common.ElementID]*v2_3.File
+ assocWithPackage map[common.ElementID]bool
+
+ // mapping of nodeStrings to parsed object to save double computation.
+ cache map[string]*nodeState
+}
+
+type Color int
+
+const (
+ GREY Color = iota // represents that the node is being visited
+ WHITE // unvisited node
+ BLACK // visited node
+)
+
+type nodeState struct {
+ // object will be pointer to the parsed or element being parsed.
+ object interface{}
+ // color of a state represents if the node is visited/unvisited/being-visited.
+ Color Color
+}
+
+type AnyLicenseInfo interface {
+ // ToLicenseString returns the representation of license about how it will
+ // be stored in the tools-golang data model
+ ToLicenseString() string
+}
+
+type SimpleLicensingInfo struct {
+ AnyLicenseInfo
+ comment string
+ licenseID string
+ name string
+ seeAlso []string
+ example string
+}
+
+type ExtractedLicensingInfo struct {
+ SimpleLicensingInfo
+ extractedText string
+}
+
+type OrLaterOperator struct {
+ AnyLicenseInfo
+ member SimpleLicensingInfo
+}
+
+type ConjunctiveLicenseSet struct {
+ AnyLicenseInfo
+ members []AnyLicenseInfo
+}
+
+type DisjunctiveLicenseSet struct {
+ AnyLicenseInfo
+ members []AnyLicenseInfo
+}
+
+type License struct {
+ SimpleLicensingInfo
+ isOsiApproved bool
+ licenseText string
+ standardLicenseHeader string
+ standardLicenseTemplate string
+ standardLicenseHeaderTemplate string
+ isDeprecatedLicenseID bool
+ isFsfLibre bool
+}
+
+type ListedLicense struct {
+ License
+}
+
+type LicenseException struct {
+ licenseExceptionId string
+ licenseExceptionText string
+ seeAlso string // must be a valid uri
+ name string
+ example string
+ comment string
+}
+
+type WithExceptionOperator struct {
+ AnyLicenseInfo
+ member SimpleLicensingInfo
+ licenseException LicenseException
+}
+
+// custom LicenseType to provide support for licences of
+// type Noassertion, None and customLicenses
+type SpecialLicense struct {
+ AnyLicenseInfo
+ value SpecialLicenseValue
+}
+
+type SpecialLicenseValue string
+
+const (
+ NONE SpecialLicenseValue = "NONE"
+ NOASSERTION SpecialLicenseValue = "NOASSERTION"
+)
+
+type RangeType string
+
+const (
+ BYTE_RANGE RangeType = "byteRange"
+ LINE_RANGE RangeType = "lineRange"
+)
diff --git a/rdfloader/parser2v3/utils.go b/rdfloader/parser2v3/utils.go
new file mode 100644
index 0000000..356471b
--- /dev/null
+++ b/rdfloader/parser2v3/utils.go
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/gordf/rdfwriter"
+ urilib "github.com/spdx/gordf/uri"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// a uri is of type baseURI#fragment or baseFragment/subFragment
+// returns fragment or subFragment when given as an input.
+func getLastPartOfURI(uri string) string {
+ if strings.Contains(uri, "#") {
+ parts := strings.Split(uri, "#")
+ return parts[len(parts)-1]
+ }
+ parts := strings.Split(uri, "/")
+ return parts[len(parts)-1]
+}
+
+func isUriValid(uri string) bool {
+ _, err := urilib.NewURIRef(uri)
+ return err == nil
+}
+
+func getNodeTypeFromTriples(triples []*gordfParser.Triple, node *gordfParser.Node) (string, error) {
+ if node == nil {
+ return "", errors.New("empty node passed to find node type")
+ }
+ typeTriples := rdfwriter.FilterTriples(triples, &node.ID, &RDF_TYPE, nil)
+ switch len(typeTriples) {
+ case 0:
+ return "", fmt.Errorf("node{%v} not associated with any type triple", node)
+ case 1:
+ return typeTriples[0].Object.ID, nil
+ default:
+ return "", fmt.Errorf("node{%v} is associated with more than one type triples", node)
+ }
+}
+
+func (parser *rdfParser2_3) nodeToTriples(node *gordfParser.Node) []*gordfParser.Triple {
+ if node == nil {
+ return []*gordfParser.Triple{}
+ }
+ return parser.nodeStringToTriples[node.String()]
+}
+
+// returns which boolean was given as an input
+// string(bool) is the only possible input for which it will not raise any error.
+func boolFromString(boolString string) (bool, error) {
+ switch strings.ToLower(boolString) {
+ case "true":
+ return true, nil
+ case "false":
+ return false, nil
+ default:
+ return false, fmt.Errorf("boolean string can be either true/false")
+ }
+}
+
+/* Function Below this line is taken from the tvloader/parser2v3/utils.go */
+
+// used to extract DocumentRef and SPDXRef values from an SPDX Identifier
+// which can point either to this document or to a different one
+func ExtractDocElementID(value string) (common.DocElementID, error) {
+ docRefID := ""
+ idStr := value
+
+ // check prefix to see if it's a DocumentRef ID
+ if strings.HasPrefix(idStr, "DocumentRef-") {
+ // extract the part that comes between "DocumentRef-" and ":"
+ strs := strings.Split(idStr, ":")
+ // should be exactly two, part before and part after
+ if len(strs) < 2 {
+ return common.DocElementID{}, fmt.Errorf("no colon found although DocumentRef- prefix present")
+ }
+ if len(strs) > 2 {
+ return common.DocElementID{}, fmt.Errorf("more than one colon found")
+ }
+
+ // trim the prefix and confirm non-empty
+ docRefID = strings.TrimPrefix(strs[0], "DocumentRef-")
+ if docRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("document identifier has nothing after prefix")
+ }
+ // and use remainder for element ID parsing
+ idStr = strs[1]
+ }
+
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(idStr, "SPDXRef-") {
+ return common.DocElementID{}, fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(idStr, ":") {
+ // we know this means there was no DocumentRef- prefix, because
+ // we would have handled multiple colons above if it was
+ return common.DocElementID{}, fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(idStr, "SPDXRef-")
+ if eltRefID == "" {
+ return common.DocElementID{}, fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.DocElementID{DocumentRefID: docRefID, ElementRefID: common.ElementID(eltRefID)}, nil
+}
+
+// used to extract SPDXRef values only from an SPDX Identifier which can point
+// to this document only. Use extractDocElementID for parsing IDs that can
+// refer either to this document or a different one.
+func ExtractElementID(value string) (common.ElementID, error) {
+ // check prefix to confirm it's got the right prefix for element IDs
+ if !strings.HasPrefix(value, "SPDXRef-") {
+ return common.ElementID(""), fmt.Errorf("missing SPDXRef- prefix for element identifier")
+ }
+
+ // make sure no colons are present
+ if strings.Contains(value, ":") {
+ return common.ElementID(""), fmt.Errorf("invalid colon in element identifier")
+ }
+
+ // trim the prefix and confirm non-empty
+ eltRefID := strings.TrimPrefix(value, "SPDXRef-")
+ if eltRefID == "" {
+ return common.ElementID(""), fmt.Errorf("element identifier has nothing after prefix")
+ }
+
+ // we're good
+ return common.ElementID(eltRefID), nil
+}
+
+// used to extract key / value from embedded substrings
+// returns subkey, subvalue, nil if no error, or "", "", error otherwise
+func ExtractSubs(value string, sep string) (string, string, error) {
+ // parse the value to see if it's a valid subvalue format
+ sp := strings.SplitN(value, sep, 2)
+ if len(sp) == 1 {
+ return "", "", fmt.Errorf("invalid subvalue format for %s (no %s found)", value, sep)
+ }
+
+ subkey := strings.TrimSpace(sp[0])
+ subvalue := strings.TrimSpace(sp[1])
+
+ return subkey, subvalue, nil
+}
diff --git a/rdfloader/parser2v3/utils_test.go b/rdfloader/parser2v3/utils_test.go
new file mode 100644
index 0000000..24aa4e9
--- /dev/null
+++ b/rdfloader/parser2v3/utils_test.go
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v3
+
+import (
+ "reflect"
+ "testing"
+
+ gordfParser "github.com/spdx/gordf/rdfloader/parser"
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+func Test_getLastPartOfURI(t *testing.T) {
+ // uri of type baseFragment#fragment
+ input := "baseFragment#fragment"
+ expectedOutput := "fragment"
+ output := getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+
+ // uri of type baseFragment/subFragment
+ input = "baseFragment/subFragment"
+ expectedOutput = "subFragment"
+ output = getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+
+ // neither of the case mustn't raise any error.
+ input = "www.github.com"
+ expectedOutput = input
+ output = getLastPartOfURI(input)
+ if output != expectedOutput {
+ t.Errorf("expected %s, found %s", expectedOutput, output)
+ }
+}
+
+func Test_isUriValid(t *testing.T) {
+ // TestCase 1: Valid Input URI
+ input := "https://www.github.com"
+ isValid := isUriValid(input)
+ if !isValid {
+ t.Errorf("valid input(%s) detected as invalid.", input)
+ }
+
+ // TestCase 2: Invalid Input URI
+ input = `http\:www.github.com`
+ isValid = isUriValid(input)
+ if isValid {
+ t.Errorf("invalid input(%s) detected as valid", input)
+ }
+}
+
+func Test_rdfParser2_3_nodeToTriples(t *testing.T) {
+ var parser *rdfParser2_3
+ var output, expectedOutput []*gordfParser.Triple
+
+ // TestCase 1: a nil node shouldn't raise any error or panic.
+ parser, _ = parserFromBodyContent(``)
+ output = parser.nodeToTriples(nil)
+ if output == nil {
+ t.Errorf("nil input should return an empty slice and not nil")
+ }
+ expectedOutput = []*gordfParser.Triple{}
+ if !reflect.DeepEqual(output, expectedOutput) {
+ t.Errorf("expected %+v, got %+v", expectedOutput, output)
+ }
+
+ // TestCase 2: node should be addressable based on the node content and not the pointer.
+ // It should allow new nodes same as the older ones to retrieve the associated triples.
+ parser, _ = parserFromBodyContent(`
+ <spdx:Checksum rdf:about="#checksum">
+ <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1" />
+ <spdx:checksumValue>75068c26abbed3ad3980685bae21d7202d288317</spdx:checksumValue>
+ </spdx:Checksum>
+ `)
+ newNode := &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#checksum",
+ }
+ output = parser.nodeToTriples(newNode)
+
+ // The output must have 3 triples:
+ // 1. newNode rdf:type Checksum
+ // 2. newNode spdx:algorithm http://spdx.org/rdf/terms#checksumAlgorithm_sha1
+ // 3. newNode spdx:checksumValue 75068c26abbed3ad3980685bae21d7202d288317
+ if len(output) != 3 {
+ t.Errorf("expected output to have 3 triples, got %d", len(output))
+ }
+}
+
+func Test_boolFromString(t *testing.T) {
+ // TestCase 1: Valid Input: "true"
+ // mustn't raise any error
+ input := "true"
+ val, err := boolFromString(input)
+ if err != nil {
+ t.Errorf("function raised an error for a valid input(%s): %s", input, err)
+ }
+ if val != true {
+ t.Errorf("invalid output. Expected %v, found %v", true, val)
+ }
+
+ // TestCase 2: Valid Input: "true"
+ // mustn't raise any error
+ input = "false"
+ val, err = boolFromString(input)
+ if err != nil {
+ t.Errorf("function raised an error for a valid input(%s): %s", input, err)
+ }
+ if val != false {
+ t.Errorf("invalid output. Expected %v, found %v", false, val)
+ }
+
+ // TestCase 3: invalid input: ""
+ // it must raise an error
+ input = ""
+ val, err = boolFromString(input)
+ if err == nil {
+ t.Errorf("invalid input should've raised an error")
+ }
+}
+
+func Test_getNodeTypeFromTriples(t *testing.T) {
+ var err error
+ var node *gordfParser.Node
+ var triples []*gordfParser.Triple
+ var nodeType, expectedNodeType string
+
+ // TestCase 1: nil node must raise an error because,
+ // nil nodes cannot be associated with any rdf:type attribute.
+ _, err = getNodeTypeFromTriples(triples, nil)
+ if err == nil {
+ t.Errorf("expected an error due to nil node, got %v", err)
+ }
+
+ // TestCase 2: none of the triples give information about the rdf:type of a node.
+ node = &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "N0",
+ }
+ _, err = getNodeTypeFromTriples(triples, node)
+ if err == nil {
+ t.Errorf("expected an error saying no rdf:type found, got %v", err)
+ }
+
+ // TestCase 3: node is associated with exactly one rdf:type triples
+ typeTriple := &gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: RDF_TYPE,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/rdf/terms#Checksum",
+ },
+ }
+ triples = append(triples, typeTriple)
+ expectedNodeType = "http://spdx.org/rdf/terms#Checksum"
+ nodeType, err = getNodeTypeFromTriples(triples, node)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if nodeType != expectedNodeType {
+ t.Errorf("expected: %v, got: %v", nodeType, expectedNodeType)
+ }
+
+ // TestCase 4: node associated with more than one rdf:type triples must raise an error.
+ typeTriple = &gordfParser.Triple{
+ Subject: node,
+ Predicate: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: RDF_TYPE,
+ },
+ Object: &gordfParser.Node{
+ NodeType: gordfParser.IRI,
+ ID: "http://spdx.org/rdf/terms#Snippet",
+ },
+ }
+ triples = append(triples, typeTriple)
+ _, err = getNodeTypeFromTriples(triples, node)
+ if err == nil {
+ t.Errorf("expected an error saying more than one rdf:type found, got %v", err)
+ }
+}
+
+// following tests are copy pasted from tvloader/parser2v3/util_test.go
+
+func TestCanExtractDocumentAndElementRefsFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractDocElementID(t, "SPDXRef-file1", false, "", "file1")
+ // test with valid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file2", false, "doc2", "file2")
+ // test with invalid ID in this document
+ helperForExtractDocElementID(t, "a:SPDXRef-file1", true, "", "")
+ helperForExtractDocElementID(t, "file1", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "SPDXRef-file1:", true, "", "")
+ // test with invalid ID in another document
+ helperForExtractDocElementID(t, "DocumentRef-doc2", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-doc2:a", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:", true, "", "")
+ helperForExtractDocElementID(t, "DocumentRef-:SPDXRef-file1", true, "", "")
+ // test with invalid formats
+ helperForExtractDocElementID(t, "DocumentRef-doc2:SPDXRef-file1:file2", true, "", "")
+}
+
+func helperForExtractDocElementID(t *testing.T, tst string, wantErr bool, wantDoc string, wantElt string) {
+ deID, err := ExtractDocElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if deID.DocumentRefID != wantDoc {
+ if wantDoc == "" {
+ t.Errorf("testing %v: want empty string for DocumentRefID, got %v", tst, deID.DocumentRefID)
+ } else {
+ t.Errorf("testing %v: want %v for DocumentRefID, got %v", tst, wantDoc, deID.DocumentRefID)
+ }
+ }
+ if deID.ElementRefID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, deID.ElementRefID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, deID.ElementRefID)
+ }
+ }
+}
+
+func TestCanExtractElementRefsOnlyFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractElementID(t, "SPDXRef-file1", false, "file1")
+ // test with valid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-file2", true, "")
+ // test with invalid ID in this document
+ helperForExtractElementID(t, "a:SPDXRef-file1", true, "")
+ helperForExtractElementID(t, "file1", true, "")
+ helperForExtractElementID(t, "SPDXRef-", true, "")
+ helperForExtractElementID(t, "SPDXRef-file1:", true, "")
+ // test with invalid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:a", true, "")
+ helperForExtractElementID(t, "DocumentRef-:", true, "")
+ helperForExtractElementID(t, "DocumentRef-:SPDXRef-file1", true, "")
+}
+
+func helperForExtractElementID(t *testing.T, tst string, wantErr bool, wantElt string) {
+ eID, err := ExtractElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if eID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, eID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, eID)
+ }
+ }
+}
+
+func TestCanExtractSubvalues(t *testing.T) {
+ subkey, subvalue, err := ExtractSubs("SHA1: abc123", ":")
+ if err != nil {
+ t.Errorf("got error when calling extractSubs: %v", err)
+ }
+ if subkey != "SHA1" {
+ t.Errorf("got %v for subkey", subkey)
+ }
+ if subvalue != "abc123" {
+ t.Errorf("got %v for subvalue", subvalue)
+ }
+}
+
+func TestReturnsErrorForInvalidSubvalueFormat(t *testing.T) {
+ _, _, err := ExtractSubs("blah", ":")
+ if err == nil {
+ t.Errorf("expected error when calling extractSubs for invalid format (0 colons), got nil")
+ }
+}
diff --git a/rdfloader/rdfloader.go b/rdfloader/rdfloader.go
new file mode 100644
index 0000000..4575799
--- /dev/null
+++ b/rdfloader/rdfloader.go
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package rdfloader
+
+import (
+ "io"
+
+ "github.com/spdx/gordf/rdfloader"
+ "github.com/spdx/tools-golang/rdfloader/parser2v2"
+ "github.com/spdx/tools-golang/rdfloader/parser2v3"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// Takes in a file Reader and returns the pertaining spdx document
+// or the error if any is encountered while setting the doc.
+func Load2_2(content io.Reader) (*v2_2.Document, error) {
+ var rdfParserObj, err = rdfloader.LoadFromReaderObject(content)
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := parser2v2.LoadFromGoRDFParser(rdfParserObj)
+ return doc, err
+}
+
+// Takes in a file Reader and returns the pertaining spdx document
+// or the error if any is encountered while setting the doc.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ var rdfParserObj, err = rdfloader.LoadFromReaderObject(content)
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := parser2v3.LoadFromGoRDFParser(rdfParserObj)
+ return doc, err
+}
diff --git a/rdfloader/rdfloader_test.go b/rdfloader/rdfloader_test.go
new file mode 100644
index 0000000..4f77bdf
--- /dev/null
+++ b/rdfloader/rdfloader_test.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package rdfloader
+
+import (
+ "io"
+ "strings"
+ "testing"
+)
+
+func TestLoad2_2(t *testing.T) {
+ var reader io.Reader
+ var err error
+
+ // TestCase 1: invalid rdf/xml must raise an error
+ reader = strings.NewReader("")
+ _, err = Load2_2(reader)
+ if err == nil {
+ t.Errorf("expected an EOF error reading an empty file, got %v", err)
+ }
+
+ // TestCase 2: Valid rdf/xml but invalid spdx document must raise an error
+ reader = strings.NewReader(`
+ <rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#">
+ </rdf:RDF>
+ `)
+ _, err = Load2_2(reader)
+ if err == nil {
+ t.Errorf("expected an error due to no SpdxDocument Node in the document")
+ }
+}
+
+func TestLoad2_3(t *testing.T) {
+ var reader io.Reader
+ var err error
+
+ // TestCase 1: invalid rdf/xml must raise an error
+ reader = strings.NewReader("")
+ _, err = Load2_3(reader)
+ if err == nil {
+ t.Errorf("expected an EOF error reading an empty file, got %v", err)
+ }
+
+ // TestCase 2: Valid rdf/xml but invalid spdx document must raise an error
+ reader = strings.NewReader(`
+ <rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#">
+ </rdf:RDF>
+ `)
+ _, err = Load2_3(reader)
+ if err == nil {
+ t.Errorf("expected an error due to no SpdxDocument Node in the document")
+ }
+
+ // TestCase 3: New SPDX package elements
+ reader = strings.NewReader(`
+ <rdf:RDF
+ xmlns:spdx="http://spdx.org/rdf/terms#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#"
+ xmlns:doap="http://usefulinc.com/ns/doap#"
+ xmlns:j.0="http://www.w3.org/2009/pointers#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+ <spdx:SpdxDocument rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DOCUMENT">
+ <spdx:specVersion>SPDX-2.0</spdx:specVersion>
+ <spdx:relationship>
+ <spdx:Relationship>
+ <spdx:relationshipType rdf:resource="http://spdx.org/rdf/terms#relationshipType_describes"/>
+ <spdx:relatedSpdxElement>
+ <spdx:Package rdf:about="http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-Saxon">
+ <spdx:name>Some-Package</spdx:name>
+ <spdx:primaryPackagePurpose rdf:resource="packagePurpose_container" />
+ <spdx:releaseDate>2021-10-15T02:38:00Z</spdx:releaseDate>
+ <spdx:builtDate>2021-09-15T02:38:00Z</spdx:builtDate>
+ <spdx:validUntilDate>2022-10-15T02:38:00Z</spdx:validUntilDate>
+ </spdx:Package>
+ </spdx:relatedSpdxElement>
+ </spdx:Relationship>
+ </spdx:relationship>
+ </spdx:SpdxDocument>
+ </rdf:RDF>
+ `)
+
+ doc, err := Load2_3(reader)
+ if err != nil {
+ t.Errorf("expected valid SPDX document: %v", err)
+ }
+
+ if doc == nil {
+ t.Fatalf("expected valid SPDX document but got nil")
+ }
+
+ if len(doc.Packages) == 0 {
+ t.Errorf("expected packages but got none")
+ }
+
+ pkg := doc.Packages[0]
+ if pkg.PackageName != "Some-Package" {
+ t.Errorf("expected package nameof Some-Package but got: %s", pkg.PackageName)
+ }
+ if pkg.PrimaryPackagePurpose != "CONTAINER" {
+ t.Errorf("expected package primary purpose of CONTAINER but got: %s", pkg.PrimaryPackagePurpose)
+ }
+ if pkg.ReleaseDate != "2021-10-15T02:38:00Z" {
+ t.Errorf("expected release date of 2021-10-15T02:38:00Z but got: %s", pkg.ReleaseDate)
+ }
+ if pkg.BuiltDate != "2021-09-15T02:38:00Z" {
+ t.Errorf("expected built date of 2021-09-15T02:38:00Z but got: %s", pkg.BuiltDate)
+ }
+ if pkg.ValidUntilDate != "2022-10-15T02:38:00Z" {
+ t.Errorf("expected valid until date of 2022-10-15T02:38:00Z but got: %s", pkg.ValidUntilDate)
+ }
+}
diff --git a/reporter/reporter.go b/reporter/reporter.go
new file mode 100644
index 0000000..0a492bc
--- /dev/null
+++ b/reporter/reporter.go
@@ -0,0 +1,205 @@
+// Package reporter contains functions to generate a basic license count
+// report from an in-memory SPDX Package section whose Files have been
+// analyzed.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package reporter
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "text/tabwriter"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 Reporter functions =====
+
+// Generate2_1 takes a Package whose Files have been analyzed and an
+// io.Writer, and outputs to the io.Writer a tabulated count of
+// the number of Files for each unique LicenseConcluded in the set.
+func Generate2_1(pkg *v2_1.Package, w io.Writer) error {
+ if pkg.FilesAnalyzed == false {
+ return fmt.Errorf("Package FilesAnalyzed is false")
+ }
+ totalFound, totalNotFound, foundCounts := countLicenses2_1(pkg)
+
+ wr := tabwriter.NewWriter(w, 0, 0, 2, ' ', tabwriter.AlignRight)
+
+ fmt.Fprintf(wr, "%d\t License found\n", totalFound)
+ fmt.Fprintf(wr, "%d\t License not found\n", totalNotFound)
+ fmt.Fprintf(wr, "%d\t TOTAL\n", totalFound+totalNotFound)
+ fmt.Fprintf(wr, "\n")
+
+ counts := []struct {
+ lic string
+ count int
+ }{}
+ for k, v := range foundCounts {
+ var entry struct {
+ lic string
+ count int
+ }
+ entry.lic = k
+ entry.count = v
+ counts = append(counts, entry)
+ }
+
+ sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
+
+ for _, c := range counts {
+ fmt.Fprintf(wr, "%d\t %s\n", c.count, c.lic)
+ }
+ fmt.Fprintf(wr, "%d\t TOTAL FOUND\n", totalFound)
+
+ wr.Flush()
+ return nil
+}
+
+func countLicenses2_1(pkg *v2_1.Package) (int, int, map[string]int) {
+ if pkg == nil || pkg.Files == nil {
+ return 0, 0, nil
+ }
+
+ totalFound := 0
+ totalNotFound := 0
+ foundCounts := map[string]int{}
+ for _, f := range pkg.Files {
+ if f.LicenseConcluded == "" || f.LicenseConcluded == "NOASSERTION" {
+ totalNotFound++
+ } else {
+ totalFound++
+ foundCounts[f.LicenseConcluded]++
+ }
+ }
+
+ return totalFound, totalNotFound, foundCounts
+}
+
+// ===== 2.2 Reporter functions =====
+
+// Generate2_2 takes a Package whose Files have been analyzed and an
+// io.Writer, and outputs to the io.Writer a tabulated count of
+// the number of Files for each unique LicenseConcluded in the set.
+func Generate2_2(pkg *v2_2.Package, w io.Writer) error {
+ if pkg.FilesAnalyzed == false {
+ return fmt.Errorf("Package FilesAnalyzed is false")
+ }
+ totalFound, totalNotFound, foundCounts := countLicenses2_2(pkg)
+
+ wr := tabwriter.NewWriter(w, 0, 0, 2, ' ', tabwriter.AlignRight)
+
+ fmt.Fprintf(wr, "%d\t License found\n", totalFound)
+ fmt.Fprintf(wr, "%d\t License not found\n", totalNotFound)
+ fmt.Fprintf(wr, "%d\t TOTAL\n", totalFound+totalNotFound)
+ fmt.Fprintf(wr, "\n")
+
+ counts := []struct {
+ lic string
+ count int
+ }{}
+ for k, v := range foundCounts {
+ var entry struct {
+ lic string
+ count int
+ }
+ entry.lic = k
+ entry.count = v
+ counts = append(counts, entry)
+ }
+
+ sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
+
+ for _, c := range counts {
+ fmt.Fprintf(wr, "%d\t %s\n", c.count, c.lic)
+ }
+ fmt.Fprintf(wr, "%d\t TOTAL FOUND\n", totalFound)
+
+ wr.Flush()
+ return nil
+}
+
+func countLicenses2_2(pkg *v2_2.Package) (int, int, map[string]int) {
+ if pkg == nil || pkg.Files == nil {
+ return 0, 0, nil
+ }
+
+ totalFound := 0
+ totalNotFound := 0
+ foundCounts := map[string]int{}
+ for _, f := range pkg.Files {
+ if f.LicenseConcluded == "" || f.LicenseConcluded == "NOASSERTION" {
+ totalNotFound++
+ } else {
+ totalFound++
+ foundCounts[f.LicenseConcluded]++
+ }
+ }
+
+ return totalFound, totalNotFound, foundCounts
+}
+
+// ===== 2.3 Reporter functions =====
+
+// Generate2_3 takes a Package whose Files have been analyzed and an
+// io.Writer, and outputs to the io.Writer a tabulated count of
+// the number of Files for each unique LicenseConcluded in the set.
+func Generate2_3(pkg *v2_3.Package, w io.Writer) error {
+ if pkg.FilesAnalyzed == false {
+ return fmt.Errorf("Package FilesAnalyzed is false")
+ }
+ totalFound, totalNotFound, foundCounts := countLicenses2_3(pkg)
+
+ wr := tabwriter.NewWriter(w, 0, 0, 2, ' ', tabwriter.AlignRight)
+
+ fmt.Fprintf(wr, "%d\t License found\n", totalFound)
+ fmt.Fprintf(wr, "%d\t License not found\n", totalNotFound)
+ fmt.Fprintf(wr, "%d\t TOTAL\n", totalFound+totalNotFound)
+ fmt.Fprintf(wr, "\n")
+
+ counts := []struct {
+ lic string
+ count int
+ }{}
+ for k, v := range foundCounts {
+ var entry struct {
+ lic string
+ count int
+ }
+ entry.lic = k
+ entry.count = v
+ counts = append(counts, entry)
+ }
+
+ sort.Slice(counts, func(i, j int) bool { return counts[i].count > counts[j].count })
+
+ for _, c := range counts {
+ fmt.Fprintf(wr, "%d\t %s\n", c.count, c.lic)
+ }
+ fmt.Fprintf(wr, "%d\t TOTAL FOUND\n", totalFound)
+
+ wr.Flush()
+ return nil
+}
+
+func countLicenses2_3(pkg *v2_3.Package) (int, int, map[string]int) {
+ if pkg == nil || pkg.Files == nil {
+ return 0, 0, nil
+ }
+
+ totalFound := 0
+ totalNotFound := 0
+ foundCounts := map[string]int{}
+ for _, f := range pkg.Files {
+ if f.LicenseConcluded == "" || f.LicenseConcluded == "NOASSERTION" {
+ totalNotFound++
+ } else {
+ totalFound++
+ foundCounts[f.LicenseConcluded]++
+ }
+ }
+
+ return totalFound, totalNotFound, foundCounts
+}
diff --git a/reporter/reporter_test.go b/reporter/reporter_test.go
new file mode 100644
index 0000000..7e6fc88
--- /dev/null
+++ b/reporter/reporter_test.go
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package reporter
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 Reporter top-level function tests =====
+func Test2_1ReporterCanMakeReportFromPackage(t *testing.T) {
+ pkg := &v2_1.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_1.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(` 9 License found
+ 3 License not found
+ 12 TOTAL
+
+ 5 GPL-2.0-only
+ 4 MIT
+ 9 TOTAL FOUND
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_1(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func Test2_1ReporterReturnsErrorIfPackageFilesNotAnalyzed(t *testing.T) {
+ pkg := &v2_1.Package{
+ FilesAnalyzed: false,
+ }
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_1(pkg, &got)
+ if err == nil {
+ t.Errorf("Expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.1 Utility functions =====
+
+func Test2_1CanGetCountsOfLicenses(t *testing.T) {
+ pkg := &v2_1.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_1.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ totalFound, totalNotFound, foundCounts := countLicenses2_1(pkg)
+ if totalFound != 9 {
+ t.Errorf("expected %v, got %v", 9, totalFound)
+ }
+ if totalNotFound != 3 {
+ t.Errorf("expected %v, got %v", 3, totalNotFound)
+ }
+ if len(foundCounts) != 2 {
+ t.Fatalf("expected %v, got %v", 2, len(foundCounts))
+ }
+
+ // foundCounts is a map of license ID to count of licenses
+ // confirm that the results are as expected
+ if foundCounts["GPL-2.0-only"] != 5 {
+ t.Errorf("expected %v, got %v", 5, foundCounts["GPL-2.0-only"])
+ }
+ if foundCounts["MIT"] != 4 {
+ t.Errorf("expected %v, got %v", 4, foundCounts["MIT"])
+ }
+}
+
+func Test2_1NilPackageReturnsZeroCountsOfLicenses(t *testing.T) {
+ totalFound, totalNotFound, foundCounts := countLicenses2_1(nil)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+
+ pkg := &v2_1.Package{}
+ totalFound, totalNotFound, foundCounts = countLicenses2_1(pkg)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+}
+
+// ===== 2.2 Reporter top-level function tests =====
+func Test2_2ReporterCanMakeReportFromPackage(t *testing.T) {
+ pkg := &v2_2.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_2.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(` 9 License found
+ 3 License not found
+ 12 TOTAL
+
+ 5 GPL-2.0-only
+ 4 MIT
+ 9 TOTAL FOUND
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_2(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func Test2_2ReporterReturnsErrorIfPackageFilesNotAnalyzed(t *testing.T) {
+ pkg := &v2_2.Package{
+ FilesAnalyzed: false,
+ }
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_2(pkg, &got)
+ if err == nil {
+ t.Errorf("Expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.2 Utility functions =====
+
+func Test2_2CanGetCountsOfLicenses(t *testing.T) {
+ pkg := &v2_2.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_2.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ totalFound, totalNotFound, foundCounts := countLicenses2_2(pkg)
+ if totalFound != 9 {
+ t.Errorf("expected %v, got %v", 9, totalFound)
+ }
+ if totalNotFound != 3 {
+ t.Errorf("expected %v, got %v", 3, totalNotFound)
+ }
+ if len(foundCounts) != 2 {
+ t.Fatalf("expected %v, got %v", 2, len(foundCounts))
+ }
+
+ // foundCounts is a map of license ID to count of licenses
+ // confirm that the results are as expected
+ if foundCounts["GPL-2.0-only"] != 5 {
+ t.Errorf("expected %v, got %v", 5, foundCounts["GPL-2.0-only"])
+ }
+ if foundCounts["MIT"] != 4 {
+ t.Errorf("expected %v, got %v", 4, foundCounts["MIT"])
+ }
+}
+
+func Test2_2NilPackageReturnsZeroCountsOfLicenses(t *testing.T) {
+ totalFound, totalNotFound, foundCounts := countLicenses2_2(nil)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+
+ pkg := &v2_2.Package{}
+ totalFound, totalNotFound, foundCounts = countLicenses2_2(pkg)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+}
+
+// ===== 2.3 Reporter top-level function tests =====
+func Test2_3ReporterCanMakeReportFromPackage(t *testing.T) {
+ pkg := &v2_3.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_3.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ // what we want to get, as a buffer of bytes
+ want := bytes.NewBufferString(` 9 License found
+ 3 License not found
+ 12 TOTAL
+
+ 5 GPL-2.0-only
+ 4 MIT
+ 9 TOTAL FOUND
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_3(pkg, &got)
+ if err != nil {
+ t.Errorf("Expected nil error, got %v", err)
+ }
+
+ // check that they match
+ c := bytes.Compare(want.Bytes(), got.Bytes())
+ if c != 0 {
+ t.Errorf("Expected %v, got %v", want.String(), got.String())
+ }
+}
+
+func Test2_3ReporterReturnsErrorIfPackageFilesNotAnalyzed(t *testing.T) {
+ pkg := &v2_3.Package{
+ FilesAnalyzed: false,
+ }
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := Generate2_3(pkg, &got)
+ if err == nil {
+ t.Errorf("Expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.3 Utility functions =====
+
+func Test2_3CanGetCountsOfLicenses(t *testing.T) {
+ pkg := &v2_3.Package{
+ FilesAnalyzed: true,
+ Files: []*v2_3.File{
+ {FileSPDXIdentifier: "File0", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File1", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File2", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File3", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File4", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File5", LicenseConcluded: "NOASSERTION"},
+ {FileSPDXIdentifier: "File6", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File7", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File8", LicenseConcluded: "MIT"},
+ {FileSPDXIdentifier: "File9", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File10", LicenseConcluded: "GPL-2.0-only"},
+ {FileSPDXIdentifier: "File11", LicenseConcluded: "NOASSERTION"},
+ },
+ }
+
+ totalFound, totalNotFound, foundCounts := countLicenses2_3(pkg)
+ if totalFound != 9 {
+ t.Errorf("expected %v, got %v", 9, totalFound)
+ }
+ if totalNotFound != 3 {
+ t.Errorf("expected %v, got %v", 3, totalNotFound)
+ }
+ if len(foundCounts) != 2 {
+ t.Fatalf("expected %v, got %v", 2, len(foundCounts))
+ }
+
+ // foundCounts is a map of license ID to count of licenses
+ // confirm that the results are as expected
+ if foundCounts["GPL-2.0-only"] != 5 {
+ t.Errorf("expected %v, got %v", 5, foundCounts["GPL-2.0-only"])
+ }
+ if foundCounts["MIT"] != 4 {
+ t.Errorf("expected %v, got %v", 4, foundCounts["MIT"])
+ }
+}
+
+func Test2_3NilPackageReturnsZeroCountsOfLicenses(t *testing.T) {
+ totalFound, totalNotFound, foundCounts := countLicenses2_3(nil)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+
+ pkg := &v2_3.Package{}
+ totalFound, totalNotFound, foundCounts = countLicenses2_3(pkg)
+ if totalFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalFound)
+ }
+ if totalNotFound != 0 {
+ t.Errorf("expected %v, got %v", 0, totalNotFound)
+ }
+ if len(foundCounts) != 0 {
+ t.Fatalf("expected %v, got %v", 0, len(foundCounts))
+ }
+}
diff --git a/spdx/common/annotation.go b/spdx/common/annotation.go
new file mode 100644
index 0000000..e77d7b7
--- /dev/null
+++ b/spdx/common/annotation.go
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+type Annotator struct {
+ Annotator string
+ // including AnnotatorType: one of "Person", "Organization" or "Tool"
+ AnnotatorType string
+}
+
+// UnmarshalJSON takes an annotator in the typical one-line format and parses it into an Annotator struct.
+// This function is also used when unmarshalling YAML
+func (a *Annotator) UnmarshalJSON(data []byte) error {
+ // annotator will simply be a string
+ annotatorStr := string(data)
+ annotatorStr = strings.Trim(annotatorStr, "\"")
+
+ annotatorFields := strings.SplitN(annotatorStr, ": ", 2)
+
+ if len(annotatorFields) != 2 {
+ return fmt.Errorf("failed to parse Annotator '%s'", annotatorStr)
+ }
+
+ a.AnnotatorType = annotatorFields[0]
+ a.Annotator = annotatorFields[1]
+
+ return nil
+}
+
+// MarshalJSON converts the receiver into a slice of bytes representing an Annotator in string form.
+// This function is also used when marshalling to YAML
+func (a Annotator) MarshalJSON() ([]byte, error) {
+ if a.Annotator != "" {
+ return json.Marshal(fmt.Sprintf("%s: %s", a.AnnotatorType, a.Annotator))
+ }
+
+ return []byte{}, nil
+}
diff --git a/spdx/common/checksum.go b/spdx/common/checksum.go
new file mode 100644
index 0000000..aa2ae52
--- /dev/null
+++ b/spdx/common/checksum.go
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+// ChecksumAlgorithm represents the algorithm used to generate the file checksum in the Checksum struct.
+type ChecksumAlgorithm string
+
+// The checksum algorithms mentioned in the spdxv2.2.0 https://spdx.github.io/spdx-spec/4-file-information/#44-file-checksum
+const (
+ SHA224 ChecksumAlgorithm = "SHA224"
+ SHA1 ChecksumAlgorithm = "SHA1"
+ SHA256 ChecksumAlgorithm = "SHA256"
+ SHA384 ChecksumAlgorithm = "SHA384"
+ SHA512 ChecksumAlgorithm = "SHA512"
+ MD2 ChecksumAlgorithm = "MD2"
+ MD4 ChecksumAlgorithm = "MD4"
+ MD5 ChecksumAlgorithm = "MD5"
+ MD6 ChecksumAlgorithm = "MD6"
+ SHA3_256 ChecksumAlgorithm = "SHA3-256"
+ SHA3_384 ChecksumAlgorithm = "SHA3-384"
+ SHA3_512 ChecksumAlgorithm = "SHA3-512"
+ BLAKE2b_256 ChecksumAlgorithm = "BLAKE2b-256"
+ BLAKE2b_384 ChecksumAlgorithm = "BLAKE2b-384"
+ BLAKE2b_512 ChecksumAlgorithm = "BLAKE2b-512"
+ BLAKE3 ChecksumAlgorithm = "BLAKE3"
+ ADLER32 ChecksumAlgorithm = "ADLER32"
+)
+
+// Checksum provides a unique identifier to match analysis information on each specific file in a package.
+// The Algorithm field describes the ChecksumAlgorithm used and the Value represents the file checksum
+type Checksum struct {
+ Algorithm ChecksumAlgorithm `json:"algorithm"`
+ Value string `json:"checksumValue"`
+}
diff --git a/spdx/common/creation_info.go b/spdx/common/creation_info.go
new file mode 100644
index 0000000..c87ae7b
--- /dev/null
+++ b/spdx/common/creation_info.go
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+// Creator is a wrapper around the Creator SPDX field. The SPDX field contains two values, which requires special
+// handling in order to marshal/unmarshal it to/from Go data types.
+type Creator struct {
+ Creator string
+ // CreatorType should be one of "Person", "Organization", or "Tool"
+ CreatorType string
+}
+
+// UnmarshalJSON takes an annotator in the typical one-line format and parses it into a Creator struct.
+// This function is also used when unmarshalling YAML
+func (c *Creator) UnmarshalJSON(data []byte) error {
+ str := string(data)
+ str = strings.Trim(str, "\"")
+ fields := strings.SplitN(str, ": ", 2)
+
+ if len(fields) != 2 {
+ return fmt.Errorf("failed to parse Creator '%s'", str)
+ }
+
+ c.CreatorType = fields[0]
+ c.Creator = fields[1]
+
+ return nil
+}
+
+// MarshalJSON converts the receiver into a slice of bytes representing a Creator in string form.
+// This function is also used with marshalling to YAML
+func (c Creator) MarshalJSON() ([]byte, error) {
+ if c.Creator != "" {
+ return json.Marshal(fmt.Sprintf("%s: %s", c.CreatorType, c.Creator))
+ }
+
+ return []byte{}, nil
+}
diff --git a/spdx/common/external.go b/spdx/common/external.go
new file mode 100644
index 0000000..59c3f0f
--- /dev/null
+++ b/spdx/common/external.go
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+// Constants for various string types
+const (
+ // F.2 Security types
+ TypeSecurityCPE23Type string = "cpe23Type"
+ TypeSecurityCPE22Type string = "cpe22Type"
+ TypeSecurityAdvisory string = "advisory"
+ TypeSecurityFix string = "fix"
+ TypeSecurityUrl string = "url"
+ TypeSecuritySwid string = "swid"
+
+ // F.3 Package-Manager types
+ TypePackageManagerMavenCentral string = "maven-central"
+ TypePackageManagerNpm string = "npm"
+ TypePackageManagerNuGet string = "nuget"
+ TypePackageManagerBower string = "bower"
+ TypePackageManagerPURL string = "purl"
+
+ // 11.1 Relationship field types
+ TypeRelationshipDescribe string = "DESCRIBES"
+ TypeRelationshipDescribeBy string = "DESCRIBED_BY"
+ TypeRelationshipContains string = "CONTAINS"
+ TypeRelationshipContainedBy string = "CONTAINED_BY"
+ TypeRelationshipDependsOn string = "DEPENDS_ON"
+ TypeRelationshipDependencyOf string = "DEPENDENCY_OF"
+ TypeRelationshipBuildDependencyOf string = "BUILD_DEPENDENCY_OF"
+ TypeRelationshipDevDependencyOf string = "DEV_DEPENDENCY_OF"
+ TypeRelationshipOptionalDependencyOf string = "OPTIONAL_DEPENDENCY_OF"
+ TypeRelationshipProvidedDependencyOf string = "PROVIDED_DEPENDENCY_OF"
+ TypeRelationshipTestDependencyOf string = "TEST_DEPENDENCY_OF"
+ TypeRelationshipRuntimeDependencyOf string = "RUNTIME_DEPENDENCY_OF"
+ TypeRelationshipExampleOf string = "EXAMPLE_OF"
+ TypeRelationshipGenerates string = "GENERATES"
+ TypeRelationshipGeneratedFrom string = "GENERATED_FROM"
+ TypeRelationshipAncestorOf string = "ANCESTOR_OF"
+ TypeRelationshipDescendantOf string = "DESCENDANT_OF"
+ TypeRelationshipVariantOf string = "VARIANT_OF"
+ TypeRelationshipDistributionArtifact string = "DISTRIBUTION_ARTIFACT"
+ TypeRelationshipPatchFor string = "PATCH_FOR"
+ TypeRelationshipPatchApplied string = "PATCH_APPLIED"
+ TypeRelationshipCopyOf string = "COPY_OF"
+ TypeRelationshipFileAdded string = "FILE_ADDED"
+ TypeRelationshipFileDeleted string = "FILE_DELETED"
+ TypeRelationshipFileModified string = "FILE_MODIFIED"
+ TypeRelationshipExpandedFromArchive string = "EXPANDED_FROM_ARCHIVE"
+ TypeRelationshipDynamicLink string = "DYNAMIC_LINK"
+ TypeRelationshipStaticLink string = "STATIC_LINK"
+ TypeRelationshipDataFileOf string = "DATA_FILE_OF"
+ TypeRelationshipTestCaseOf string = "TEST_CASE_OF"
+ TypeRelationshipBuildToolOf string = "BUILD_TOOL_OF"
+ TypeRelationshipDevToolOf string = "DEV_TOOL_OF"
+ TypeRelationshipTestOf string = "TEST_OF"
+ TypeRelationshipTestToolOf string = "TEST_TOOL_OF"
+ TypeRelationshipDocumentationOf string = "DOCUMENTATION_OF"
+ TypeRelationshipOptionalComponentOf string = "OPTIONAL_COMPONENT_OF"
+ TypeRelationshipMetafileOf string = "METAFILE_OF"
+ TypeRelationshipPackageOf string = "PACKAGE_OF"
+ TypeRelationshipAmends string = "AMENDS"
+ TypeRelationshipPrerequisiteFor string = "PREREQUISITE_FOR"
+ TypeRelationshipHasPrerequisite string = "HAS_PREREQUISITE"
+ TypeRelationshipRequirementDescriptionFor string = "REQUIREMENT_DESCRIPTION_FOR"
+ TypeRelationshipSpecificationFor string = "SPECIFICATION_FOR"
+ TypeRelationshipOther string = "OTHER"
+)
diff --git a/spdx/common/identifier.go b/spdx/common/identifier.go
new file mode 100644
index 0000000..806a815
--- /dev/null
+++ b/spdx/common/identifier.go
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+const (
+ spdxRefPrefix = "SPDXRef-"
+ documentRefPrefix = "DocumentRef-"
+)
+
+// ElementID represents the identifier string portion of an SPDX element
+// identifier. DocElementID should be used for any attributes which can
+// contain identifiers defined in a different SPDX document.
+// ElementIDs should NOT contain the mandatory 'SPDXRef-' portion.
+type ElementID string
+
+// MarshalJSON returns an SPDXRef- prefixed JSON string
+func (d ElementID) MarshalJSON() ([]byte, error) {
+ return json.Marshal(prefixElementId(d))
+}
+
+// UnmarshalJSON validates SPDXRef- prefixes and removes them when processing ElementIDs
+func (d *ElementID) UnmarshalJSON(data []byte) error {
+ // SPDX identifier will simply be a string
+ idStr := string(data)
+ idStr = strings.Trim(idStr, "\"")
+
+ e, err := trimElementIdPrefix(idStr)
+ if err != nil {
+ return err
+ }
+ *d = e
+ return nil
+}
+
+// prefixElementId adds the SPDXRef- prefix to an element ID if it does not have one
+func prefixElementId(id ElementID) string {
+ val := string(id)
+ if !strings.HasPrefix(val, spdxRefPrefix) {
+ return spdxRefPrefix + val
+ }
+ return val
+}
+
+// trimElementIdPrefix removes the SPDXRef- prefix from an element ID string or returns an error if it
+// does not start with SPDXRef-
+func trimElementIdPrefix(id string) (ElementID, error) {
+ // handle SPDXRef-
+ idFields := strings.SplitN(id, spdxRefPrefix, 2)
+ if len(idFields) != 2 {
+ return "", fmt.Errorf("failed to parse SPDX identifier '%s'", id)
+ }
+
+ e := ElementID(idFields[1])
+ return e, nil
+}
+
+// DocElementID represents an SPDX element identifier that could be defined
+// in a different SPDX document, and therefore could have a "DocumentRef-"
+// portion, such as Relationships and Annotations.
+// ElementID is used for attributes in which a "DocumentRef-" portion cannot
+// appear, such as a Package or File definition (since it is necessarily
+// being defined in the present document).
+// DocumentRefID will be the empty string for elements defined in the
+// present document.
+// DocElementIDs should NOT contain the mandatory 'DocumentRef-' or
+// 'SPDXRef-' portions.
+// SpecialID is used ONLY if the DocElementID matches a defined set of
+// permitted special values for a particular field, e.g. "NONE" or
+// "NOASSERTION" for the right-hand side of Relationships. If SpecialID
+// is set, DocumentRefID and ElementRefID should be empty (and vice versa).
+type DocElementID struct {
+ DocumentRefID string
+ ElementRefID ElementID
+ SpecialID string
+}
+
+// MarshalJSON converts the receiver into a slice of bytes representing a DocElementID in string form.
+// This function is also used when marshalling to YAML
+func (d DocElementID) MarshalJSON() ([]byte, error) {
+ if d.DocumentRefID != "" && d.ElementRefID != "" {
+ idStr := prefixElementId(d.ElementRefID)
+ return json.Marshal(fmt.Sprintf("%s%s:%s", documentRefPrefix, d.DocumentRefID, idStr))
+ } else if d.ElementRefID != "" {
+ return json.Marshal(prefixElementId(d.ElementRefID))
+ } else if d.SpecialID != "" {
+ return json.Marshal(d.SpecialID)
+ }
+
+ return []byte{}, fmt.Errorf("failed to marshal empty DocElementID")
+}
+
+// UnmarshalJSON takes a SPDX Identifier string parses it into a DocElementID struct.
+// This function is also used when unmarshalling YAML
+func (d *DocElementID) UnmarshalJSON(data []byte) (err error) {
+ // SPDX identifier will simply be a string
+ idStr := string(data)
+ idStr = strings.Trim(idStr, "\"")
+
+ // handle special cases
+ if idStr == "NONE" || idStr == "NOASSERTION" {
+ d.SpecialID = idStr
+ return nil
+ }
+
+ var idFields []string
+ // handle DocumentRef- if present
+ if strings.HasPrefix(idStr, documentRefPrefix) {
+ // strip out the "DocumentRef-" so we can get the value
+ idFields = strings.SplitN(idStr, documentRefPrefix, 2)
+ idStr = idFields[1]
+
+ // an SPDXRef can appear after a DocumentRef, separated by a colon
+ idFields = strings.SplitN(idStr, ":", 2)
+ d.DocumentRefID = idFields[0]
+
+ if len(idFields) == 2 {
+ idStr = idFields[1]
+ } else {
+ return nil
+ }
+ }
+
+ d.ElementRefID, err = trimElementIdPrefix(idStr)
+ return err
+}
+
+// TODO: add equivalents for LicenseRef- identifiers
+
+// MakeDocElementID takes strings (without prefixes) for the DocumentRef-
+// and SPDXRef- identifiers, and returns a DocElementID. An empty string
+// should be used for the DocumentRef- portion if it is referring to the
+// present document.
+func MakeDocElementID(docRef string, eltRef string) DocElementID {
+ return DocElementID{
+ DocumentRefID: docRef,
+ ElementRefID: ElementID(eltRef),
+ }
+}
+
+// MakeDocElementSpecial takes a "special" string (e.g. "NONE" or
+// "NOASSERTION" for the right side of a Relationship), nd returns
+// a DocElementID with it in the SpecialID field. Other fields will
+// be empty.
+func MakeDocElementSpecial(specialID string) DocElementID {
+ return DocElementID{SpecialID: specialID}
+}
+
+// RenderElementID takes an ElementID and returns the string equivalent,
+// with the SPDXRef- prefix reinserted.
+func RenderElementID(eID ElementID) string {
+ return spdxRefPrefix + string(eID)
+}
+
+// RenderDocElementID takes a DocElementID and returns the string equivalent,
+// with the SPDXRef- prefix (and, if applicable, the DocumentRef- prefix)
+// reinserted. If a SpecialID is present, it will be rendered verbatim and
+// DocumentRefID and ElementRefID will be ignored.
+func RenderDocElementID(deID DocElementID) string {
+ if deID.SpecialID != "" {
+ return deID.SpecialID
+ }
+ prefix := ""
+ if deID.DocumentRefID != "" {
+ prefix = documentRefPrefix + deID.DocumentRefID + ":"
+ }
+ return prefix + spdxRefPrefix + string(deID.ElementRefID)
+}
diff --git a/spdx/common/identifier_test.go b/spdx/common/identifier_test.go
new file mode 100644
index 0000000..8df2ea3
--- /dev/null
+++ b/spdx/common/identifier_test.go
@@ -0,0 +1,314 @@
+package common
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func Test_DocElementIDEncoding(t *testing.T) {
+ tests := []struct {
+ name string
+ value DocElementID
+ expected string
+ err bool
+ }{
+ {
+ name: "ElementRefID",
+ value: DocElementID{
+ ElementRefID: "some-id",
+ },
+ expected: "SPDXRef-some-id",
+ },
+ {
+ name: "DocumentRefID:ElementRefID",
+ value: DocElementID{
+ DocumentRefID: "a-doc",
+ ElementRefID: "some-id",
+ },
+ expected: "DocumentRef-a-doc:SPDXRef-some-id",
+ },
+ {
+ name: "DocumentRefID no ElementRefID",
+ value: DocElementID{
+ DocumentRefID: "a-doc",
+ },
+ err: true,
+ },
+ {
+ name: "SpecialID",
+ value: DocElementID{
+ SpecialID: "special-id",
+ },
+ expected: "special-id",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result, err := json.Marshal(test.value)
+ switch {
+ case !test.err && err != nil:
+ t.Fatalf("unexpected error: %v", err)
+ case test.err && err == nil:
+ t.Fatalf("expected error but got none")
+ case test.err:
+ return
+ }
+ s := string(result)
+ if !strings.HasPrefix(s, `"`) || !strings.HasSuffix(s, `"`) {
+ t.Fatalf("string was not returned: %s", s)
+ }
+ s = strings.Trim(s, `"`)
+ if test.expected != s {
+ t.Fatalf("%s != %s", test.expected, s)
+ }
+ })
+ }
+}
+
+func Test_DocElementIDDecoding(t *testing.T) {
+ tests := []struct {
+ name string
+ value string
+ expected DocElementID
+ err bool
+ }{
+ {
+ name: "ElementRefID",
+ value: "SPDXRef-some-id",
+ expected: DocElementID{
+ ElementRefID: "some-id",
+ },
+ },
+ {
+ name: "DocumentRefID:ElementRefID",
+ value: "DocumentRef-a-doc:SPDXRef-some-id",
+ expected: DocElementID{
+ DocumentRefID: "a-doc",
+ ElementRefID: "some-id",
+ },
+ },
+ {
+ name: "DocumentRefID no ElementRefID",
+ value: "DocumentRef-a-doc",
+ expected: DocElementID{
+ DocumentRefID: "a-doc",
+ },
+ },
+ {
+ name: "DocumentRefID invalid ElementRefID",
+ value: "DocumentRef-a-doc:invalid",
+ err: true,
+ },
+ {
+ name: "invalid format",
+ value: "some-id-without-spdxref",
+ err: true,
+ },
+ {
+ name: "SpecialID NONE",
+ value: "NONE",
+ expected: DocElementID{
+ SpecialID: "NONE",
+ },
+ },
+ {
+ name: "SpecialID NOASSERTION",
+ value: "NOASSERTION",
+ expected: DocElementID{
+ SpecialID: "NOASSERTION",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ out := DocElementID{}
+ s := fmt.Sprintf(`"%s"`, test.value)
+ err := json.Unmarshal([]byte(s), &out)
+ switch {
+ case !test.err && err != nil:
+ t.Fatalf("unexpected error: %v", err)
+ case test.err && err == nil:
+ t.Fatalf("expected error but got none")
+ case test.err:
+ return
+ }
+ if !reflect.DeepEqual(test.expected, out) {
+ t.Fatalf("unexpected value: %v != %v", test.expected, out)
+ }
+ })
+ }
+}
+
+func Test_ElementIDEncoding(t *testing.T) {
+ tests := []struct {
+ name string
+ value ElementID
+ expected string
+ err bool
+ }{
+ {
+ name: "appends spdxref",
+ value: ElementID("some-id"),
+ expected: "SPDXRef-some-id",
+ },
+ {
+ name: "appends spdxref",
+ value: ElementID("SPDXRef-some-id"),
+ expected: "SPDXRef-some-id",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result, err := json.Marshal(test.value)
+ switch {
+ case !test.err && err != nil:
+ t.Fatalf("unexpected error: %v", err)
+ case test.err && err == nil:
+ t.Fatalf("expected error but got none")
+ case test.err:
+ return
+ }
+ s := string(result)
+ if !strings.HasPrefix(s, `"`) || !strings.HasSuffix(s, `"`) {
+ t.Fatalf("string was not returned: %s", s)
+ }
+ s = strings.Trim(s, `"`)
+ if test.expected != s {
+ t.Fatalf("%s != %s", test.expected, s)
+ }
+ })
+ }
+}
+
+func Test_ElementIDDecoding(t *testing.T) {
+ tests := []struct {
+ name string
+ value string
+ expected ElementID
+ err bool
+ }{
+ {
+ name: "valid id",
+ value: "SPDXRef-some-id",
+ expected: ElementID("some-id"),
+ },
+ {
+ name: "invalid format",
+ value: "some-id-without-spdxref",
+ err: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var out ElementID
+ s := fmt.Sprintf(`"%s"`, test.value)
+ err := json.Unmarshal([]byte(s), &out)
+ switch {
+ case !test.err && err != nil:
+ t.Fatalf("unexpected error: %v", err)
+ case test.err && err == nil:
+ t.Fatalf("expected error but got none")
+ case test.err:
+ return
+ }
+ if !reflect.DeepEqual(test.expected, out) {
+ t.Fatalf("unexpected value: %v != %v", test.expected, out)
+ }
+ })
+ }
+}
+
+func Test_ElementIDStructEncoding(t *testing.T) {
+ type typ struct {
+ Id ElementID `json:"id"`
+ }
+ tests := []struct {
+ name string
+ value typ
+ expected string
+ err bool
+ }{
+ {
+ name: "appends spdxref",
+ value: typ{
+ Id: ElementID("some-id"),
+ },
+ expected: `{"id":"SPDXRef-some-id"}`,
+ },
+ {
+ name: "appends spdxref",
+ value: typ{
+ Id: ElementID("SPDXRef-some-id"),
+ },
+ expected: `{"id":"SPDXRef-some-id"}`,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result, err := json.Marshal(test.value)
+ switch {
+ case !test.err && err != nil:
+ t.Fatalf("unexpected error: %v", err)
+ case test.err && err == nil:
+ t.Fatalf("expected error but got none")
+ case test.err:
+ return
+ }
+ s := string(result)
+ if test.expected != s {
+ t.Fatalf("%s != %s", test.expected, s)
+ }
+ })
+ }
+}
+
+func Test_ElementIDStructDecoding(t *testing.T) {
+ type typ struct {
+ Id ElementID `json:"id"`
+ }
+ tests := []struct {
+ name string
+ value string
+ expected typ
+ err bool
+ }{
+ {
+ name: "valid id",
+ expected: typ{
+ Id: ElementID("some-id"),
+ },
+ value: `{"id":"SPDXRef-some-id"}`,
+ },
+ {
+ name: "invalid format",
+ value: `{"id":"some-id"}`,
+ err: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ out := typ{}
+ err := json.Unmarshal([]byte(test.value), &out)
+ switch {
+ case !test.err && err != nil:
+ t.Fatalf("unexpected error: %v", err)
+ case test.err && err == nil:
+ t.Fatalf("expected error but got none")
+ case test.err:
+ return
+ }
+ if !reflect.DeepEqual(test.expected, out) {
+ t.Fatalf("unexpected value: %v != %v", test.expected, out)
+ }
+ })
+ }
+}
diff --git a/spdx/common/package.go b/spdx/common/package.go
new file mode 100644
index 0000000..de5a075
--- /dev/null
+++ b/spdx/common/package.go
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+)
+
+type Supplier struct {
+ // can be "NOASSERTION"
+ Supplier string
+ // SupplierType can be one of "Person", "Organization", or empty if Supplier is "NOASSERTION"
+ SupplierType string
+}
+
+// UnmarshalJSON takes a supplier in the typical one-line format and parses it into a Supplier struct.
+// This function is also used when unmarshalling YAML
+func (s *Supplier) UnmarshalJSON(data []byte) error {
+ // the value is just a string presented as a slice of bytes
+ supplierStr := string(data)
+ supplierStr = strings.Trim(supplierStr, "\"")
+
+ if supplierStr == "NOASSERTION" {
+ s.Supplier = supplierStr
+ return nil
+ }
+
+ supplierFields := strings.SplitN(supplierStr, ": ", 2)
+
+ if len(supplierFields) != 2 {
+ return fmt.Errorf("failed to parse Supplier '%s'", supplierStr)
+ }
+
+ s.SupplierType = supplierFields[0]
+ s.Supplier = supplierFields[1]
+
+ return nil
+}
+
+// MarshalJSON converts the receiver into a slice of bytes representing a Supplier in string form.
+// This function is also used when marshalling to YAML
+func (s Supplier) MarshalJSON() ([]byte, error) {
+ if s.Supplier == "NOASSERTION" {
+ return json.Marshal(s.Supplier)
+ } else if s.SupplierType != "" && s.Supplier != "" {
+ return json.Marshal(fmt.Sprintf("%s: %s", s.SupplierType, s.Supplier))
+ }
+
+ return []byte{}, fmt.Errorf("failed to marshal invalid Supplier: %+v", s)
+}
+
+type Originator struct {
+ // can be "NOASSERTION"
+ Originator string
+ // OriginatorType can be one of "Person", "Organization", or empty if Originator is "NOASSERTION"
+ OriginatorType string
+}
+
+// UnmarshalJSON takes an originator in the typical one-line format and parses it into an Originator struct.
+// This function is also used when unmarshalling YAML
+func (o *Originator) UnmarshalJSON(data []byte) error {
+ // the value is just a string presented as a slice of bytes
+ originatorStr := string(data)
+ originatorStr = strings.Trim(originatorStr, "\"")
+
+ if originatorStr == "NOASSERTION" {
+ o.Originator = originatorStr
+ return nil
+ }
+
+ originatorFields := strings.SplitN(originatorStr, ": ", 2)
+
+ if len(originatorFields) != 2 {
+ return fmt.Errorf("failed to parse Originator '%s'", originatorStr)
+ }
+
+ o.OriginatorType = originatorFields[0]
+ o.Originator = originatorFields[1]
+
+ return nil
+}
+
+// MarshalJSON converts the receiver into a slice of bytes representing an Originator in string form.
+// This function is also used when marshalling to YAML
+func (o Originator) MarshalJSON() ([]byte, error) {
+ if o.Originator == "NOASSERTION" {
+ return json.Marshal(o.Originator)
+ } else if o.Originator != "" {
+ return json.Marshal(fmt.Sprintf("%s: %s", o.OriginatorType, o.Originator))
+ }
+
+ return []byte{}, nil
+}
+
+type PackageVerificationCode struct {
+ // Cardinality: mandatory, one if filesAnalyzed is true / omitted;
+ // zero (must be omitted) if filesAnalyzed is false
+ Value string `json:"packageVerificationCodeValue"`
+ // Spec also allows specifying files to exclude from the
+ // verification code algorithm; intended to enable exclusion of
+ // the SPDX document file itself.
+ ExcludedFiles []string `json:"packageVerificationCodeExcludedFiles,omitempty"`
+}
diff --git a/spdx/common/snippet.go b/spdx/common/snippet.go
new file mode 100644
index 0000000..63afac3
--- /dev/null
+++ b/spdx/common/snippet.go
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package common
+
+type SnippetRangePointer struct {
+ // 5.3: Snippet Byte Range: [start byte]:[end byte]
+ // Cardinality: mandatory, one
+ Offset int `json:"offset,omitempty"`
+
+ // 5.4: Snippet Line Range: [start line]:[end line]
+ // Cardinality: optional, one
+ LineNumber int `json:"lineNumber,omitempty"`
+
+ FileSPDXIdentifier ElementID `json:"reference"`
+}
+
+type SnippetRange struct {
+ StartPointer SnippetRangePointer `json:"startPointer"`
+ EndPointer SnippetRangePointer `json:"endPointer"`
+}
diff --git a/spdx/v2_1/annotation.go b/spdx/v2_1/annotation.go
new file mode 100644
index 0000000..45fcd13
--- /dev/null
+++ b/spdx/v2_1/annotation.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Annotation is an Annotation section of an SPDX Document for version 2.1 of the spec.
+type Annotation struct {
+ // 8.1: Annotator
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ Annotator common.Annotator `json:"annotator"`
+
+ // 8.2: Annotation Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationDate string `json:"annotationDate"`
+
+ // 8.3: Annotation Type: "REVIEW" or "OTHER"
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationType string `json:"annotationType"`
+
+ // 8.4: SPDX Identifier Reference
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ // This field is not used in hierarchical data formats where the referenced element is clear, such as JSON or YAML.
+ AnnotationSPDXIdentifier common.DocElementID `json:"-"`
+
+ // 8.5: Annotation Comment
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationComment string `json:"comment"`
+}
diff --git a/spdx/v2_1/creation_info.go b/spdx/v2_1/creation_info.go
new file mode 100644
index 0000000..0e1bd87
--- /dev/null
+++ b/spdx/v2_1/creation_info.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// CreationInfo is a Document Creation Information section of an
+// SPDX Document for version 2.1 of the spec.
+type CreationInfo struct {
+ // 2.7: License List Version
+ // Cardinality: optional, one
+ LicenseListVersion string `json:"licenseListVersion"`
+
+ // 2.8: Creators: may have multiple keys for Person, Organization
+ // and/or Tool
+ // Cardinality: mandatory, one or many
+ Creators []common.Creator `json:"creators"`
+
+ // 2.9: Created: data format YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: mandatory, one
+ Created string `json:"created"`
+
+ // 2.10: Creator Comment
+ // Cardinality: optional, one
+ CreatorComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_1/document.go b/spdx/v2_1/document.go
new file mode 100644
index 0000000..81738ff
--- /dev/null
+++ b/spdx/v2_1/document.go
@@ -0,0 +1,65 @@
+// Package spdx contains the struct definition for an SPDX Document
+// and its constituent parts.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// ExternalDocumentRef is a reference to an external SPDX document
+// as defined in section 2.6 for version 2.1 of the spec.
+type ExternalDocumentRef struct {
+ // DocumentRefID is the ID string defined in the start of the
+ // reference. It should _not_ contain the "DocumentRef-" part
+ // of the mandatory ID string.
+ DocumentRefID string `json:"externalDocumentId"`
+
+ // URI is the URI defined for the external document
+ URI string `json:"spdxDocument"`
+
+ // Checksum is the actual hash data
+ Checksum common.Checksum `json:"checksum"`
+}
+
+// Document is an SPDX Document for version 2.1 of the spec.
+// See https://spdx.org/sites/cpstandard/files/pages/files/spdxversion2.1.pdf
+type Document struct {
+ // 2.1: SPDX Version; should be in the format "SPDX-2.1"
+ // Cardinality: mandatory, one
+ SPDXVersion string `json:"spdxVersion"`
+
+ // 2.2: Data License; should be "CC0-1.0"
+ // Cardinality: mandatory, one
+ DataLicense string `json:"dataLicense"`
+
+ // 2.3: SPDX Identifier; should be "DOCUMENT" to represent
+ // mandatory identifier of SPDXRef-DOCUMENT
+ // Cardinality: mandatory, one
+ SPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 2.4: Document Name
+ // Cardinality: mandatory, one
+ DocumentName string `json:"name"`
+
+ // 2.5: Document Namespace
+ // Cardinality: mandatory, one
+ DocumentNamespace string `json:"documentNamespace"`
+
+ // 2.6: External Document References
+ // Cardinality: optional, one or many
+ ExternalDocumentReferences []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
+
+ // 2.11: Document Comment
+ // Cardinality: optional, one
+ DocumentComment string `json:"comment,omitempty"`
+
+ CreationInfo *CreationInfo `json:"creationInfo"`
+ Packages []*Package `json:"packages,omitempty"`
+ Files []*File `json:"files,omitempty"`
+ OtherLicenses []*OtherLicense `json:"hasExtractedLicensingInfos,omitempty"`
+ Relationships []*Relationship `json:"relationships,omitempty"`
+ Annotations []*Annotation `json:"annotations,omitempty"`
+ Snippets []Snippet `json:"snippets,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ Reviews []*Review `json:"-"`
+}
diff --git a/spdx/v2_1/file.go b/spdx/v2_1/file.go
new file mode 100644
index 0000000..2373887
--- /dev/null
+++ b/spdx/v2_1/file.go
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// File is a File section of an SPDX Document for version 2.1 of the spec.
+type File struct {
+ // 4.1: File Name
+ // Cardinality: mandatory, one
+ FileName string `json:"fileName"`
+
+ // 4.2: File SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ FileSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 4.3: File Types
+ // Cardinality: optional, multiple
+ FileTypes []string `json:"fileTypes,omitempty"`
+
+ // 4.4: File Checksum: may have keys for SHA1, SHA256 and/or MD5
+ // Cardinality: mandatory, one SHA1, others may be optionally provided
+ Checksums []common.Checksum `json:"checksums"`
+
+ // 4.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ LicenseConcluded string `json:"licenseConcluded"`
+
+ // 4.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one or many
+ LicenseInfoInFiles []string `json:"licenseInfoInFiles"`
+
+ // 4.7: Comments on License
+ // Cardinality: optional, one
+ LicenseComments string `json:"licenseComments,omitempty"`
+
+ // 4.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ FileCopyrightText string `json:"copyrightText"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 4.9-4.11: Artifact of Project variables (defined below)
+ // Cardinality: optional, one or many
+ ArtifactOfProjects []*ArtifactOfProject `json:"-"`
+
+ // 4.12: File Comment
+ // Cardinality: optional, one
+ FileComment string `json:"comment,omitempty"`
+
+ // 4.13: File Notice
+ // Cardinality: optional, one
+ FileNotice string `json:"noticeText,omitempty"`
+
+ // 4.14: File Contributor
+ // Cardinality: optional, one or many
+ FileContributors []string `json:"fileContributors,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ // 4.15: File Dependencies
+ // Cardinality: optional, one or many
+ FileDependencies []string `json:"-"`
+
+ // Snippets contained in this File
+ // Note that Snippets could be defined in a different Document! However,
+ // the only ones that _THIS_ document can contain are the ones that are
+ // defined here -- so this should just be an ElementID.
+ Snippets map[common.ElementID]*Snippet `json:"-"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// ArtifactOfProject is a DEPRECATED collection of data regarding
+// a Package, as defined in sections 4.9-4.11 in version 2.1 of the spec.
+type ArtifactOfProject struct {
+
+ // DEPRECATED in version 2.1 of spec
+ // 4.9: Artifact of Project Name
+ // Cardinality: conditional, required if present, one per AOP
+ Name string
+
+ // DEPRECATED in version 2.1 of spec
+ // 4.10: Artifact of Project Homepage: URL or "UNKNOWN"
+ // Cardinality: optional, one per AOP
+ HomePage string
+
+ // DEPRECATED in version 2.1 of spec
+ // 4.11: Artifact of Project Uniform Resource Identifier
+ // Cardinality: optional, one per AOP
+ URI string
+}
diff --git a/spdx/v2_1/other_license.go b/spdx/v2_1/other_license.go
new file mode 100644
index 0000000..6ae09fe
--- /dev/null
+++ b/spdx/v2_1/other_license.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+// OtherLicense is an Other License Information section of an
+// SPDX Document for version 2.1 of the spec.
+type OtherLicense struct {
+ // 6.1: License Identifier: "LicenseRef-[idstring]"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseIdentifier string `json:"licenseId"`
+
+ // 6.2: Extracted Text
+ // Cardinality: conditional (mandatory, one) if there is a
+ // License Identifier assigned
+ ExtractedText string `json:"extractedText"`
+
+ // 6.3: License Name: single line of text or "NOASSERTION"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseName string `json:"name,omitempty"`
+
+ // 6.4: License Cross Reference
+ // Cardinality: conditional (optional, one or many) if license
+ // is not on SPDX License List
+ LicenseCrossReferences []string `json:"seeAlsos,omitempty"`
+
+ // 6.5: License Comment
+ // Cardinality: optional, one
+ LicenseComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_1/package.go b/spdx/v2_1/package.go
new file mode 100644
index 0000000..6b0d7a5
--- /dev/null
+++ b/spdx/v2_1/package.go
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Package is a Package section of an SPDX Document for version 2.1 of the spec.
+type Package struct {
+ // 3.1: Package Name
+ // Cardinality: mandatory, one
+ PackageName string `json:"name"`
+
+ // 3.2: Package SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ PackageSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 3.3: Package Version
+ // Cardinality: optional, one
+ PackageVersion string `json:"versionInfo,omitempty"`
+
+ // 3.4: Package File Name
+ // Cardinality: optional, one
+ PackageFileName string `json:"packageFileName,omitempty"`
+
+ // 3.5: Package Supplier: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageSupplier *common.Supplier `json:"supplier,omitempty"`
+
+ // 3.6: Package Originator: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageOriginator *common.Originator `json:"originator,omitempty"`
+
+ // 3.7: Package Download Location
+ // Cardinality: mandatory, one
+ PackageDownloadLocation string `json:"downloadLocation"`
+
+ // 3.8: FilesAnalyzed
+ // Cardinality: optional, one; default value is "true" if omitted
+ FilesAnalyzed bool `json:"filesAnalyzed,omitempty"`
+ // NOT PART OF SPEC: did FilesAnalyzed tag appear?
+ IsFilesAnalyzedTagPresent bool `json:"-"`
+
+ // 3.9: Package Verification Code
+ PackageVerificationCode common.PackageVerificationCode `json:"packageVerificationCode"`
+
+ // 3.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
+ // Cardinality: optional, one or many
+ PackageChecksums []common.Checksum `json:"checksums,omitempty"`
+
+ // 3.11: Package Home Page
+ // Cardinality: optional, one
+ PackageHomePage string `json:"homepage,omitempty"`
+
+ // 3.12: Source Information
+ // Cardinality: optional, one
+ PackageSourceInfo string `json:"sourceInfo,omitempty"`
+
+ // 3.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageLicenseConcluded string `json:"licenseConcluded"`
+
+ // 3.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
+ // zero (must be omitted) if filesAnalyzed is false
+ PackageLicenseInfoFromFiles []string `json:"licenseInfoFromFiles"`
+
+ // 3.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageLicenseDeclared string `json:"licenseDeclared"`
+
+ // 3.16: Comments on License
+ // Cardinality: optional, one
+ PackageLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageCopyrightText string `json:"copyrightText"`
+
+ // 3.18: Package Summary Description
+ // Cardinality: optional, one
+ PackageSummary string `json:"summary,omitempty"`
+
+ // 3.19: Package Detailed Description
+ // Cardinality: optional, one
+ PackageDescription string `json:"description,omitempty"`
+
+ // 3.20: Package Comment
+ // Cardinality: optional, one
+ PackageComment string `json:"comment,omitempty"`
+
+ // 3.21: Package External Reference
+ // Cardinality: optional, one or many
+ PackageExternalReferences []*PackageExternalReference `json:"externalRefs,omitempty"`
+
+ // Files contained in this Package
+ Files []*File `json:"files,omitempty"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// PackageExternalReference is an External Reference to additional info
+// about a Package, as defined in section 3.21 in version 2.1 of the spec.
+type PackageExternalReference struct {
+ // category is "SECURITY", "PACKAGE-MANAGER" or "OTHER"
+ Category string `json:"referenceCategory"`
+
+ // type is an [idstring] as defined in Appendix VI;
+ // called RefType here due to "type" being a Golang keyword
+ RefType string `json:"referenceType"`
+
+ // locator is a unique string to access the package-specific
+ // info, metadata or content within the target location
+ Locator string `json:"referenceLocator"`
+
+ // 3.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ ExternalRefComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_1/relationship.go b/spdx/v2_1/relationship.go
new file mode 100644
index 0000000..006e23f
--- /dev/null
+++ b/spdx/v2_1/relationship.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Relationship is a Relationship section of an SPDX Document for
+// version 2.1 of the spec.
+type Relationship struct {
+
+ // 7.1: Relationship
+ // Cardinality: optional, one or more; one per Relationship
+ // one mandatory for SPDX Document with multiple packages
+ // RefA and RefB are first and second item
+ // Relationship is type from 7.1.1
+ RefA common.DocElementID `json:"spdxElementId"`
+ RefB common.DocElementID `json:"relatedSpdxElement"`
+ Relationship string `json:"relationshipType"`
+
+ // 7.2: Relationship Comment
+ // Cardinality: optional, one
+ RelationshipComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_1/review.go b/spdx/v2_1/review.go
new file mode 100644
index 0000000..8d70d00
--- /dev/null
+++ b/spdx/v2_1/review.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+// Review is a Review section of an SPDX Document for version 2.1 of the spec.
+// DEPRECATED in version 2.0 of spec; retained here for compatibility.
+type Review struct {
+
+ // DEPRECATED in version 2.0 of spec
+ // 9.1: Reviewer
+ // Cardinality: optional, one
+ Reviewer string
+ // including AnnotatorType: one of "Person", "Organization" or "Tool"
+ ReviewerType string
+
+ // DEPRECATED in version 2.0 of spec
+ // 9.2: Review Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is a Reviewer
+ ReviewDate string
+
+ // DEPRECATED in version 2.0 of spec
+ // 9.3: Review Comment
+ // Cardinality: optional, one
+ ReviewComment string
+}
diff --git a/spdx/v2_1/snippet.go b/spdx/v2_1/snippet.go
new file mode 100644
index 0000000..e4d2f59
--- /dev/null
+++ b/spdx/v2_1/snippet.go
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_1
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Snippet is a Snippet section of an SPDX Document for version 2.1 of the spec.
+type Snippet struct {
+
+ // 5.1: Snippet SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ SnippetSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 5.2: Snippet from File SPDX Identifier
+ // Cardinality: mandatory, one
+ SnippetFromFileSPDXIdentifier common.ElementID `json:"snippetFromFile"`
+
+ // Ranges denotes the start/end byte offsets or line numbers that the snippet is relevant to
+ Ranges []common.SnippetRange `json:"ranges"`
+
+ // 5.5: Snippet Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ SnippetLicenseConcluded string `json:"licenseConcluded"`
+
+ // 5.6: License Information in Snippet: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many
+ LicenseInfoInSnippet []string `json:"licenseInfoInSnippets,omitempty"`
+
+ // 5.7: Snippet Comments on License
+ // Cardinality: optional, one
+ SnippetLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 5.8: Snippet Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ SnippetCopyrightText string `json:"copyrightText"`
+
+ // 5.9: Snippet Comment
+ // Cardinality: optional, one
+ SnippetComment string `json:"comment,omitempty"`
+
+ // 5.10: Snippet Name
+ // Cardinality: optional, one
+ SnippetName string `json:"name,omitempty"`
+}
diff --git a/spdx/v2_2/annotation.go b/spdx/v2_2/annotation.go
new file mode 100644
index 0000000..35eddc6
--- /dev/null
+++ b/spdx/v2_2/annotation.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Annotation is an Annotation section of an SPDX Document for version 2.2 of the spec.
+type Annotation struct {
+ // 12.1: Annotator
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ Annotator common.Annotator `json:"annotator"`
+
+ // 12.2: Annotation Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationDate string `json:"annotationDate"`
+
+ // 12.3: Annotation Type: "REVIEW" or "OTHER"
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationType string `json:"annotationType"`
+
+ // 12.4: SPDX Identifier Reference
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ // This field is not used in hierarchical data formats where the referenced element is clear, such as JSON or YAML.
+ AnnotationSPDXIdentifier common.DocElementID `json:"-"`
+
+ // 12.5: Annotation Comment
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationComment string `json:"comment"`
+}
diff --git a/spdx/v2_2/creation_info.go b/spdx/v2_2/creation_info.go
new file mode 100644
index 0000000..70e611f
--- /dev/null
+++ b/spdx/v2_2/creation_info.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// CreationInfo is a Document Creation Information section of an
+// SPDX Document for version 2.2 of the spec.
+type CreationInfo struct {
+ // 6.7: License List Version
+ // Cardinality: optional, one
+ LicenseListVersion string `json:"licenseListVersion"`
+
+ // 6.8: Creators: may have multiple keys for Person, Organization
+ // and/or Tool
+ // Cardinality: mandatory, one or many
+ Creators []common.Creator `json:"creators"`
+
+ // 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: mandatory, one
+ Created string `json:"created"`
+
+ // 6.10: Creator Comment
+ // Cardinality: optional, one
+ CreatorComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_2/document.go b/spdx/v2_2/document.go
new file mode 100644
index 0000000..31ac08b
--- /dev/null
+++ b/spdx/v2_2/document.go
@@ -0,0 +1,65 @@
+// Package spdx contains the struct definition for an SPDX Document
+// and its constituent parts.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// ExternalDocumentRef is a reference to an external SPDX document
+// as defined in section 6.6 for version 2.2 of the spec.
+type ExternalDocumentRef struct {
+ // DocumentRefID is the ID string defined in the start of the
+ // reference. It should _not_ contain the "DocumentRef-" part
+ // of the mandatory ID string.
+ DocumentRefID string `json:"externalDocumentId"`
+
+ // URI is the URI defined for the external document
+ URI string `json:"spdxDocument"`
+
+ // Checksum is the actual hash data
+ Checksum common.Checksum `json:"checksum"`
+}
+
+// Document is an SPDX Document for version 2.2 of the spec.
+// See https://spdx.github.io/spdx-spec/v2-draft/ (DRAFT)
+type Document struct {
+ // 6.1: SPDX Version; should be in the format "SPDX-2.2"
+ // Cardinality: mandatory, one
+ SPDXVersion string `json:"spdxVersion"`
+
+ // 6.2: Data License; should be "CC0-1.0"
+ // Cardinality: mandatory, one
+ DataLicense string `json:"dataLicense"`
+
+ // 6.3: SPDX Identifier; should be "DOCUMENT" to represent
+ // mandatory identifier of SPDXRef-DOCUMENT
+ // Cardinality: mandatory, one
+ SPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 6.4: Document Name
+ // Cardinality: mandatory, one
+ DocumentName string `json:"name"`
+
+ // 6.5: Document Namespace
+ // Cardinality: mandatory, one
+ DocumentNamespace string `json:"documentNamespace"`
+
+ // 6.6: External Document References
+ // Cardinality: optional, one or many
+ ExternalDocumentReferences []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
+
+ // 6.11: Document Comment
+ // Cardinality: optional, one
+ DocumentComment string `json:"comment,omitempty"`
+
+ CreationInfo *CreationInfo `json:"creationInfo"`
+ Packages []*Package `json:"packages,omitempty"`
+ Files []*File `json:"files,omitempty"`
+ OtherLicenses []*OtherLicense `json:"hasExtractedLicensingInfos,omitempty"`
+ Relationships []*Relationship `json:"relationships,omitempty"`
+ Annotations []*Annotation `json:"annotations,omitempty"`
+ Snippets []Snippet `json:"snippets,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ Reviews []*Review `json:"-"`
+}
diff --git a/spdx/v2_2/file.go b/spdx/v2_2/file.go
new file mode 100644
index 0000000..150e79f
--- /dev/null
+++ b/spdx/v2_2/file.go
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// File is a File section of an SPDX Document for version 2.2 of the spec.
+type File struct {
+ // 8.1: File Name
+ // Cardinality: mandatory, one
+ FileName string `json:"fileName"`
+
+ // 8.2: File SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ FileSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 8.3: File Types
+ // Cardinality: optional, multiple
+ FileTypes []string `json:"fileTypes,omitempty"`
+
+ // 8.4: File Checksum: may have keys for SHA1, SHA256 and/or MD5
+ // Cardinality: mandatory, one SHA1, others may be optionally provided
+ Checksums []common.Checksum `json:"checksums"`
+
+ // 8.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ LicenseConcluded string `json:"licenseConcluded"`
+
+ // 8.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one or many
+ LicenseInfoInFiles []string `json:"licenseInfoInFiles"`
+
+ // 8.7: Comments on License
+ // Cardinality: optional, one
+ LicenseComments string `json:"licenseComments,omitempty"`
+
+ // 8.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ FileCopyrightText string `json:"copyrightText"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.9-8.11: Artifact of Project variables (defined below)
+ // Cardinality: optional, one or many
+ ArtifactOfProjects []*ArtifactOfProject `json:"-"`
+
+ // 8.12: File Comment
+ // Cardinality: optional, one
+ FileComment string `json:"comment,omitempty"`
+
+ // 8.13: File Notice
+ // Cardinality: optional, one
+ FileNotice string `json:"noticeText,omitempty"`
+
+ // 8.14: File Contributor
+ // Cardinality: optional, one or many
+ FileContributors []string `json:"fileContributors,omitempty"`
+
+ // 8.15: File Attribution Text
+ // Cardinality: optional, one or many
+ FileAttributionTexts []string `json:"attributionTexts,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ // 8.16: File Dependencies
+ // Cardinality: optional, one or many
+ FileDependencies []string `json:"-"`
+
+ // Snippets contained in this File
+ // Note that Snippets could be defined in a different Document! However,
+ // the only ones that _THIS_ document can contain are this ones that are
+ // defined here -- so this should just be an ElementID.
+ Snippets map[common.ElementID]*Snippet `json:"-"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// ArtifactOfProject is a DEPRECATED collection of data regarding
+// a Package, as defined in sections 8.9-8.11 in version 2.2 of the spec.
+type ArtifactOfProject struct {
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.9: Artifact of Project Name
+ // Cardinality: conditional, required if present, one per AOP
+ Name string
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.10: Artifact of Project Homepage: URL or "UNKNOWN"
+ // Cardinality: optional, one per AOP
+ HomePage string
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.11: Artifact of Project Uniform Resource Identifier
+ // Cardinality: optional, one per AOP
+ URI string
+}
diff --git a/spdx/v2_2/other_license.go b/spdx/v2_2/other_license.go
new file mode 100644
index 0000000..1eaf048
--- /dev/null
+++ b/spdx/v2_2/other_license.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+// OtherLicense is an Other License Information section of an
+// SPDX Document for version 2.2 of the spec.
+type OtherLicense struct {
+ // 10.1: License Identifier: "LicenseRef-[idstring]"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseIdentifier string `json:"licenseId"`
+
+ // 10.2: Extracted Text
+ // Cardinality: conditional (mandatory, one) if there is a
+ // License Identifier assigned
+ ExtractedText string `json:"extractedText"`
+
+ // 10.3: License Name: single line of text or "NOASSERTION"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseName string `json:"name,omitempty"`
+
+ // 10.4: License Cross Reference
+ // Cardinality: conditional (optional, one or many) if license
+ // is not on SPDX License List
+ LicenseCrossReferences []string `json:"seeAlsos,omitempty"`
+
+ // 10.5: License Comment
+ // Cardinality: optional, one
+ LicenseComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_2/package.go b/spdx/v2_2/package.go
new file mode 100644
index 0000000..2d99e04
--- /dev/null
+++ b/spdx/v2_2/package.go
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Package is a Package section of an SPDX Document for version 2.2 of the spec.
+type Package struct {
+ // NOT PART OF SPEC
+ // flag: does this "package" contain files that were in fact "unpackaged",
+ // e.g. included directly in the Document without being in a Package?
+ IsUnpackaged bool `json:"-"`
+
+ // 7.1: Package Name
+ // Cardinality: mandatory, one
+ PackageName string `json:"name"`
+
+ // 7.2: Package SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ PackageSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 7.3: Package Version
+ // Cardinality: optional, one
+ PackageVersion string `json:"versionInfo,omitempty"`
+
+ // 7.4: Package File Name
+ // Cardinality: optional, one
+ PackageFileName string `json:"packageFileName,omitempty"`
+
+ // 7.5: Package Supplier: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageSupplier *common.Supplier `json:"supplier,omitempty"`
+
+ // 7.6: Package Originator: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageOriginator *common.Originator `json:"originator,omitempty"`
+
+ // 7.7: Package Download Location
+ // Cardinality: mandatory, one
+ PackageDownloadLocation string `json:"downloadLocation"`
+
+ // 7.8: FilesAnalyzed
+ // Cardinality: optional, one; default value is "true" if omitted
+ FilesAnalyzed bool `json:"filesAnalyzed,omitempty"`
+ // NOT PART OF SPEC: did FilesAnalyzed tag appear?
+ IsFilesAnalyzedTagPresent bool `json:"-"`
+
+ // 7.9: Package Verification Code
+ PackageVerificationCode common.PackageVerificationCode `json:"packageVerificationCode"`
+
+ // 7.10: Package Checksum: may have keys for SHA1, SHA256, SHA512 and/or MD5
+ // Cardinality: optional, one or many
+ PackageChecksums []common.Checksum `json:"checksums,omitempty"`
+
+ // 7.11: Package Home Page
+ // Cardinality: optional, one
+ PackageHomePage string `json:"homepage,omitempty"`
+
+ // 7.12: Source Information
+ // Cardinality: optional, one
+ PackageSourceInfo string `json:"sourceInfo,omitempty"`
+
+ // 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageLicenseConcluded string `json:"licenseConcluded"`
+
+ // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
+ // zero (must be omitted) if filesAnalyzed is false
+ PackageLicenseInfoFromFiles []string `json:"licenseInfoFromFiles"`
+
+ // 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageLicenseDeclared string `json:"licenseDeclared"`
+
+ // 7.16: Comments on License
+ // Cardinality: optional, one
+ PackageLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageCopyrightText string `json:"copyrightText"`
+
+ // 7.18: Package Summary Description
+ // Cardinality: optional, one
+ PackageSummary string `json:"summary,omitempty"`
+
+ // 7.19: Package Detailed Description
+ // Cardinality: optional, one
+ PackageDescription string `json:"description,omitempty"`
+
+ // 7.20: Package Comment
+ // Cardinality: optional, one
+ PackageComment string `json:"comment,omitempty"`
+
+ // 7.21: Package External Reference
+ // Cardinality: optional, one or many
+ PackageExternalReferences []*PackageExternalReference `json:"externalRefs,omitempty"`
+
+ // 7.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ // contained within PackageExternalReference2_1 struct, if present
+
+ // 7.23: Package Attribution Text
+ // Cardinality: optional, one or many
+ PackageAttributionTexts []string `json:"attributionTexts,omitempty"`
+
+ // Files contained in this Package
+ Files []*File `json:"files,omitempty"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// PackageExternalReference is an External Reference to additional info
+// about a Package, as defined in section 7.21 in version 2.2 of the spec.
+type PackageExternalReference struct {
+ // category is "SECURITY", "PACKAGE-MANAGER" or "OTHER"
+ Category string `json:"referenceCategory"`
+
+ // type is an [idstring] as defined in Appendix VI;
+ // called RefType here due to "type" being a Golang keyword
+ RefType string `json:"referenceType"`
+
+ // locator is a unique string to access the package-specific
+ // info, metadata or content within the target location
+ Locator string `json:"referenceLocator"`
+
+ // 7.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ ExternalRefComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_2/relationship.go b/spdx/v2_2/relationship.go
new file mode 100644
index 0000000..a93baa7
--- /dev/null
+++ b/spdx/v2_2/relationship.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Relationship is a Relationship section of an SPDX Document for
+// version 2.2 of the spec.
+type Relationship struct {
+
+ // 11.1: Relationship
+ // Cardinality: optional, one or more; one per Relationship
+ // one mandatory for SPDX Document with multiple packages
+ // RefA and RefB are first and second item
+ // Relationship is type from 11.1.1
+ RefA common.DocElementID `json:"spdxElementId"`
+ RefB common.DocElementID `json:"relatedSpdxElement"`
+ Relationship string `json:"relationshipType"`
+
+ // 11.2: Relationship Comment
+ // Cardinality: optional, one
+ RelationshipComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_2/review.go b/spdx/v2_2/review.go
new file mode 100644
index 0000000..22b3b8a
--- /dev/null
+++ b/spdx/v2_2/review.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+// Review is a Review section of an SPDX Document for version 2.2 of the spec.
+// DEPRECATED in version 2.0 of spec; retained here for compatibility.
+type Review struct {
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.1: Reviewer
+ // Cardinality: optional, one
+ Reviewer string
+ // including AnnotatorType: one of "Person", "Organization" or "Tool"
+ ReviewerType string
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.2: Review Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is a Reviewer
+ ReviewDate string
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.3: Review Comment
+ // Cardinality: optional, one
+ ReviewComment string
+}
diff --git a/spdx/v2_2/snippet.go b/spdx/v2_2/snippet.go
new file mode 100644
index 0000000..61045f1
--- /dev/null
+++ b/spdx/v2_2/snippet.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_2
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Snippet is a Snippet section of an SPDX Document for version 2.2 of the spec.
+type Snippet struct {
+
+ // 9.1: Snippet SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ SnippetSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 9.2: Snippet from File SPDX Identifier
+ // Cardinality: mandatory, one
+ SnippetFromFileSPDXIdentifier common.ElementID `json:"snippetFromFile"`
+
+ // Ranges denotes the start/end byte offsets or line numbers that the snippet is relevant to
+ Ranges []common.SnippetRange `json:"ranges"`
+
+ // 9.5: Snippet Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ SnippetLicenseConcluded string `json:"licenseConcluded"`
+
+ // 9.6: License Information in Snippet: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many
+ LicenseInfoInSnippet []string `json:"licenseInfoInSnippets,omitempty"`
+
+ // 9.7: Snippet Comments on License
+ // Cardinality: optional, one
+ SnippetLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 9.8: Snippet Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ SnippetCopyrightText string `json:"copyrightText"`
+
+ // 9.9: Snippet Comment
+ // Cardinality: optional, one
+ SnippetComment string `json:"comment,omitempty"`
+
+ // 9.10: Snippet Name
+ // Cardinality: optional, one
+ SnippetName string `json:"name,omitempty"`
+
+ // 9.11: Snippet Attribution Text
+ // Cardinality: optional, one or many
+ SnippetAttributionTexts []string `json:"-"`
+}
diff --git a/spdx/v2_3/annotation.go b/spdx/v2_3/annotation.go
new file mode 100644
index 0000000..121e995
--- /dev/null
+++ b/spdx/v2_3/annotation.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Annotation is an Annotation section of an SPDX Document for version 2.3 of the spec.
+type Annotation struct {
+ // 12.1: Annotator
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ Annotator common.Annotator `json:"annotator"`
+
+ // 12.2: Annotation Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationDate string `json:"annotationDate"`
+
+ // 12.3: Annotation Type: "REVIEW" or "OTHER"
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationType string `json:"annotationType"`
+
+ // 12.4: SPDX Identifier Reference
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ // This field is not used in hierarchical data formats where the referenced element is clear, such as JSON or YAML.
+ AnnotationSPDXIdentifier common.DocElementID `json:"-" yaml:"-"`
+
+ // 12.5: Annotation Comment
+ // Cardinality: conditional (mandatory, one) if there is an Annotation
+ AnnotationComment string `json:"comment"`
+}
diff --git a/spdx/v2_3/creation_info.go b/spdx/v2_3/creation_info.go
new file mode 100644
index 0000000..33b2caf
--- /dev/null
+++ b/spdx/v2_3/creation_info.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// CreationInfo is a Document Creation Information section of an
+// SPDX Document for version 2.3 of the spec.
+type CreationInfo struct {
+ // 6.7: License List Version
+ // Cardinality: optional, one
+ LicenseListVersion string `json:"licenseListVersion"`
+
+ // 6.8: Creators: may have multiple keys for Person, Organization
+ // and/or Tool
+ // Cardinality: mandatory, one or many
+ Creators []common.Creator `json:"creators"`
+
+ // 6.9: Created: data format YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: mandatory, one
+ Created string `json:"created"`
+
+ // 6.10: Creator Comment
+ // Cardinality: optional, one
+ CreatorComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_3/document.go b/spdx/v2_3/document.go
new file mode 100644
index 0000000..32fdb8d
--- /dev/null
+++ b/spdx/v2_3/document.go
@@ -0,0 +1,65 @@
+// Package spdx contains the struct definition for an SPDX Document
+// and its constituent parts.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// ExternalDocumentRef is a reference to an external SPDX document
+// as defined in section 6.6 for version 2.3 of the spec.
+type ExternalDocumentRef struct {
+ // DocumentRefID is the ID string defined in the start of the
+ // reference. It should _not_ contain the "DocumentRef-" part
+ // of the mandatory ID string.
+ DocumentRefID string `json:"externalDocumentId"`
+
+ // URI is the URI defined for the external document
+ URI string `json:"spdxDocument"`
+
+ // Checksum is the actual hash data
+ Checksum common.Checksum `json:"checksum"`
+}
+
+// Document is an SPDX Document for version 2.3 of the spec.
+// See https://spdx.github.io/spdx-spec/v2.3/document-creation-information
+type Document struct {
+ // 6.1: SPDX Version; should be in the format "SPDX-2.3"
+ // Cardinality: mandatory, one
+ SPDXVersion string `json:"spdxVersion"`
+
+ // 6.2: Data License; should be "CC0-1.0"
+ // Cardinality: mandatory, one
+ DataLicense string `json:"dataLicense"`
+
+ // 6.3: SPDX Identifier; should be "DOCUMENT" to represent
+ // mandatory identifier of SPDXRef-DOCUMENT
+ // Cardinality: mandatory, one
+ SPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 6.4: Document Name
+ // Cardinality: mandatory, one
+ DocumentName string `json:"name"`
+
+ // 6.5: Document Namespace
+ // Cardinality: mandatory, one
+ DocumentNamespace string `json:"documentNamespace"`
+
+ // 6.6: External Document References
+ // Cardinality: optional, one or many
+ ExternalDocumentReferences []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
+
+ // 6.11: Document Comment
+ // Cardinality: optional, one
+ DocumentComment string `json:"comment,omitempty"`
+
+ CreationInfo *CreationInfo `json:"creationInfo"`
+ Packages []*Package `json:"packages,omitempty"`
+ Files []*File `json:"files,omitempty"`
+ OtherLicenses []*OtherLicense `json:"hasExtractedLicensingInfos,omitempty"`
+ Relationships []*Relationship `json:"relationships,omitempty"`
+ Annotations []*Annotation `json:"annotations,omitempty"`
+ Snippets []Snippet `json:"snippets,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ Reviews []*Review `json:"-" yaml:"-"`
+}
diff --git a/spdx/v2_3/file.go b/spdx/v2_3/file.go
new file mode 100644
index 0000000..c472fdb
--- /dev/null
+++ b/spdx/v2_3/file.go
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// File is a File section of an SPDX Document for version 2.3 of the spec.
+type File struct {
+ // 8.1: File Name
+ // Cardinality: mandatory, one
+ FileName string `json:"fileName"`
+
+ // 8.2: File SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ FileSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 8.3: File Types
+ // Cardinality: optional, multiple
+ FileTypes []string `json:"fileTypes,omitempty"`
+
+ // 8.4: File Checksum: may have keys for SHA1, SHA256, MD5, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, BLAKE3, ADLER32
+ // Cardinality: mandatory, one SHA1, others may be optionally provided
+ Checksums []common.Checksum `json:"checksums"`
+
+ // 8.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ LicenseConcluded string `json:"licenseConcluded,omitempty"`
+
+ // 8.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many
+ LicenseInfoInFiles []string `json:"licenseInfoInFiles,omitempty"`
+
+ // 8.7: Comments on License
+ // Cardinality: optional, one
+ LicenseComments string `json:"licenseComments,omitempty"`
+
+ // 8.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ FileCopyrightText string `json:"copyrightText"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.9-8.11: Artifact of Project variables (defined below)
+ // Cardinality: optional, one or many
+ ArtifactOfProjects []*ArtifactOfProject `json:"artifactOfs,omitempty"`
+
+ // 8.12: File Comment
+ // Cardinality: optional, one
+ FileComment string `json:"comment,omitempty"`
+
+ // 8.13: File Notice
+ // Cardinality: optional, one
+ FileNotice string `json:"noticeText,omitempty"`
+
+ // 8.14: File Contributor
+ // Cardinality: optional, one or many
+ FileContributors []string `json:"fileContributors,omitempty"`
+
+ // 8.15: File Attribution Text
+ // Cardinality: optional, one or many
+ FileAttributionTexts []string `json:"attributionTexts,omitempty"`
+
+ // DEPRECATED in version 2.0 of spec
+ // 8.16: File Dependencies
+ // Cardinality: optional, one or many
+ FileDependencies []string `json:"fileDependencies,omitempty"`
+
+ // Snippets contained in this File
+ // Note that Snippets could be defined in a different Document! However,
+ // the only ones that _THIS_ document can contain are this ones that are
+ // defined here -- so this should just be an ElementID.
+ Snippets map[common.ElementID]*Snippet `json:"-" yaml:"-"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// ArtifactOfProject is a DEPRECATED collection of data regarding
+// a Package, as defined in sections 8.9-8.11 in version 2.3 of the spec.
+// NOTE: the JSON schema does not define the structure of this object:
+// https://github.com/spdx/spdx-spec/blob/development/v2.3.1/schemas/spdx-schema.json#L480
+type ArtifactOfProject struct {
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.9: Artifact of Project Name
+ // Cardinality: conditional, required if present, one per AOP
+ Name string `json:"name"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.10: Artifact of Project Homepage: URL or "UNKNOWN"
+ // Cardinality: optional, one per AOP
+ HomePage string `json:"homePage"`
+
+ // DEPRECATED in version 2.1 of spec
+ // 8.11: Artifact of Project Uniform Resource Identifier
+ // Cardinality: optional, one per AOP
+ URI string `json:"URI"`
+}
diff --git a/spdx/v2_3/other_license.go b/spdx/v2_3/other_license.go
new file mode 100644
index 0000000..363bb41
--- /dev/null
+++ b/spdx/v2_3/other_license.go
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+// OtherLicense is an Other License Information section of an
+// SPDX Document for version 2.3 of the spec.
+type OtherLicense struct {
+ // 10.1: License Identifier: "LicenseRef-[idstring]"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseIdentifier string `json:"licenseId"`
+
+ // 10.2: Extracted Text
+ // Cardinality: conditional (mandatory, one) if there is a
+ // License Identifier assigned
+ ExtractedText string `json:"extractedText"`
+
+ // 10.3: License Name: single line of text or "NOASSERTION"
+ // Cardinality: conditional (mandatory, one) if license is not
+ // on SPDX License List
+ LicenseName string `json:"name,omitempty"`
+
+ // 10.4: License Cross Reference
+ // Cardinality: conditional (optional, one or many) if license
+ // is not on SPDX License List
+ LicenseCrossReferences []string `json:"seeAlsos,omitempty"`
+
+ // 10.5: License Comment
+ // Cardinality: optional, one
+ LicenseComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_3/package.go b/spdx/v2_3/package.go
new file mode 100644
index 0000000..b9d5b95
--- /dev/null
+++ b/spdx/v2_3/package.go
@@ -0,0 +1,151 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Package is a Package section of an SPDX Document for version 2.3 of the spec.
+type Package struct {
+ // NOT PART OF SPEC
+ // flag: does this "package" contain files that were in fact "unpackaged",
+ // e.g. included directly in the Document without being in a Package?
+ IsUnpackaged bool `json:"-" yaml:"-"`
+
+ // 7.1: Package Name
+ // Cardinality: mandatory, one
+ PackageName string `json:"name"`
+
+ // 7.2: Package SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ PackageSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 7.3: Package Version
+ // Cardinality: optional, one
+ PackageVersion string `json:"versionInfo,omitempty"`
+
+ // 7.4: Package File Name
+ // Cardinality: optional, one
+ PackageFileName string `json:"packageFileName,omitempty"`
+
+ // 7.5: Package Supplier: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageSupplier *common.Supplier `json:"supplier,omitempty"`
+
+ // 7.6: Package Originator: may have single result for either Person or Organization,
+ // or NOASSERTION
+ // Cardinality: optional, one
+ PackageOriginator *common.Originator `json:"originator,omitempty"`
+
+ // 7.7: Package Download Location
+ // Cardinality: mandatory, one
+ PackageDownloadLocation string `json:"downloadLocation"`
+
+ // 7.8: FilesAnalyzed
+ // Cardinality: optional, one; default value is "true" if omitted
+ FilesAnalyzed bool `json:"filesAnalyzed,omitempty"`
+ // NOT PART OF SPEC: did FilesAnalyzed tag appear?
+ IsFilesAnalyzedTagPresent bool `json:"-" yaml:"-"`
+
+ // 7.9: Package Verification Code
+ // Cardinality: if FilesAnalyzed == true must be present, if FilesAnalyzed == false must be omitted
+ PackageVerificationCode *common.PackageVerificationCode `json:"packageVerificationCode,omitempty"`
+
+ // 7.10: Package Checksum: may have keys for SHA1, SHA256, SHA512, MD5, SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, BLAKE3, ADLER32
+ // Cardinality: optional, one or many
+ PackageChecksums []common.Checksum `json:"checksums,omitempty"`
+
+ // 7.11: Package Home Page
+ // Cardinality: optional, one
+ PackageHomePage string `json:"homepage,omitempty"`
+
+ // 7.12: Source Information
+ // Cardinality: optional, one
+ PackageSourceInfo string `json:"sourceInfo,omitempty"`
+
+ // 7.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ PackageLicenseConcluded string `json:"licenseConcluded,omitempty"`
+
+ // 7.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many if filesAnalyzed is true / omitted;
+ // zero (must be omitted) if filesAnalyzed is false
+ PackageLicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"`
+
+ // 7.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ PackageLicenseDeclared string `json:"licenseDeclared,omitempty"`
+
+ // 7.16: Comments on License
+ // Cardinality: optional, one
+ PackageLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 7.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ PackageCopyrightText string `json:"copyrightText"`
+
+ // 7.18: Package Summary Description
+ // Cardinality: optional, one
+ PackageSummary string `json:"summary,omitempty"`
+
+ // 7.19: Package Detailed Description
+ // Cardinality: optional, one
+ PackageDescription string `json:"description,omitempty"`
+
+ // 7.20: Package Comment
+ // Cardinality: optional, one
+ PackageComment string `json:"comment,omitempty"`
+
+ // 7.21: Package External Reference
+ // Cardinality: optional, one or many
+ PackageExternalReferences []*PackageExternalReference `json:"externalRefs,omitempty"`
+
+ // 7.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ // contained within PackageExternalReference2_1 struct, if present
+
+ // 7.23: Package Attribution Text
+ // Cardinality: optional, one or many
+ PackageAttributionTexts []string `json:"attributionTexts,omitempty"`
+
+ // 7.24: Primary Package Purpose
+ // Cardinality: optional, one or many
+ // Allowed values: APPLICATION, FRAMEWORK, LIBRARY, CONTAINER, OPERATING-SYSTEM, DEVICE, FIRMWARE, SOURCE, ARCHIVE, FILE, INSTALL, OTHER
+ PrimaryPackagePurpose string `json:"primaryPackagePurpose,omitempty"`
+
+ // 7.25: Release Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: optional, one
+ ReleaseDate string `json:"releaseDate,omitempty"`
+
+ // 7.26: Build Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: optional, one
+ BuiltDate string `json:"builtDate,omitempty"`
+
+ // 7.27: Valid Until Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: optional, one
+ ValidUntilDate string `json:"validUntilDate,omitempty"`
+
+ // Files contained in this Package
+ Files []*File `json:"files,omitempty"`
+
+ Annotations []Annotation `json:"annotations,omitempty"`
+}
+
+// PackageExternalReference is an External Reference to additional info
+// about a Package, as defined in section 7.21 in version 2.3 of the spec.
+type PackageExternalReference struct {
+ // category is "SECURITY", "PACKAGE-MANAGER" or "OTHER"
+ Category string `json:"referenceCategory"`
+
+ // type is an [idstring] as defined in Appendix VI;
+ // called RefType here due to "type" being a Golang keyword
+ RefType string `json:"referenceType"`
+
+ // locator is a unique string to access the package-specific
+ // info, metadata or content within the target location
+ Locator string `json:"referenceLocator"`
+
+ // 7.22: Package External Reference Comment
+ // Cardinality: conditional (optional, one) for each External Reference
+ ExternalRefComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_3/relationship.go b/spdx/v2_3/relationship.go
new file mode 100644
index 0000000..af4c07d
--- /dev/null
+++ b/spdx/v2_3/relationship.go
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Relationship is a Relationship section of an SPDX Document for
+// version 2.3 of the spec.
+type Relationship struct {
+
+ // 11.1: Relationship
+ // Cardinality: optional, one or more; one per Relationship
+ // one mandatory for SPDX Document with multiple packages
+ // RefA and RefB are first and second item
+ // Relationship is type from 11.1.1
+ RefA common.DocElementID `json:"spdxElementId"`
+ RefB common.DocElementID `json:"relatedSpdxElement"`
+ Relationship string `json:"relationshipType"`
+
+ // 11.2: Relationship Comment
+ // Cardinality: optional, one
+ RelationshipComment string `json:"comment,omitempty"`
+}
diff --git a/spdx/v2_3/review.go b/spdx/v2_3/review.go
new file mode 100644
index 0000000..0463807
--- /dev/null
+++ b/spdx/v2_3/review.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+// Review is a Review section of an SPDX Document for version 2.3 of the spec.
+// DEPRECATED in version 2.0 of spec; retained here for compatibility.
+type Review struct {
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.1: Reviewer
+ // Cardinality: optional, one
+ Reviewer string
+ // including AnnotatorType: one of "Person", "Organization" or "Tool"
+ ReviewerType string
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.2: Review Date: YYYY-MM-DDThh:mm:ssZ
+ // Cardinality: conditional (mandatory, one) if there is a Reviewer
+ ReviewDate string
+
+ // DEPRECATED in version 2.0 of spec
+ // 13.3: Review Comment
+ // Cardinality: optional, one
+ ReviewComment string
+}
diff --git a/spdx/v2_3/snippet.go b/spdx/v2_3/snippet.go
new file mode 100644
index 0000000..d55a1a9
--- /dev/null
+++ b/spdx/v2_3/snippet.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package v2_3
+
+import "github.com/spdx/tools-golang/spdx/common"
+
+// Snippet is a Snippet section of an SPDX Document for version 2.3 of the spec.
+type Snippet struct {
+
+ // 9.1: Snippet SPDX Identifier: "SPDXRef-[idstring]"
+ // Cardinality: mandatory, one
+ SnippetSPDXIdentifier common.ElementID `json:"SPDXID"`
+
+ // 9.2: Snippet from File SPDX Identifier
+ // Cardinality: mandatory, one
+ SnippetFromFileSPDXIdentifier common.ElementID `json:"snippetFromFile"`
+
+ // Ranges denotes the start/end byte offsets or line numbers that the snippet is relevant to
+ Ranges []common.SnippetRange `json:"ranges"`
+
+ // 9.5: Snippet Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one
+ SnippetLicenseConcluded string `json:"licenseConcluded,omitempty"`
+
+ // 9.6: License Information in Snippet: SPDX License Expression, "NONE" or "NOASSERTION"
+ // Cardinality: optional, one or many
+ LicenseInfoInSnippet []string `json:"licenseInfoInSnippets,omitempty"`
+
+ // 9.7: Snippet Comments on License
+ // Cardinality: optional, one
+ SnippetLicenseComments string `json:"licenseComments,omitempty"`
+
+ // 9.8: Snippet Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
+ // Cardinality: mandatory, one
+ SnippetCopyrightText string `json:"copyrightText"`
+
+ // 9.9: Snippet Comment
+ // Cardinality: optional, one
+ SnippetComment string `json:"comment,omitempty"`
+
+ // 9.10: Snippet Name
+ // Cardinality: optional, one
+ SnippetName string `json:"name,omitempty"`
+
+ // 9.11: Snippet Attribution Text
+ // Cardinality: optional, one or many
+ SnippetAttributionTexts []string `json:"-" yaml:"-"`
+}
diff --git a/spdxlib/described_elements.go b/spdxlib/described_elements.go
new file mode 100644
index 0000000..a2a6356
--- /dev/null
+++ b/spdxlib/described_elements.go
@@ -0,0 +1,166 @@
+// Package spdxlib contains convenience and utility functions for working
+// with an SPDX document that has already been created in memory.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package spdxlib
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// GetDescribedPackageIDs2_1 returns a slice of ElementIDs for all Packages
+// in this Document that it "describes," according to SPDX rules:
+// - If the document has only one Package, its ID is returned.
+// - If the document has 2+ Packages, it returns the IDs of those that have
+// a DESCRIBES (or DESCRIBED_BY) relationship to this DOCUMENT.
+func GetDescribedPackageIDs2_1(doc *v2_1.Document) ([]common.ElementID, error) {
+ // if nil Packages map or zero packages in it, return empty slice
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("Packages map is nil")
+ }
+ if len(doc.Packages) == 0 {
+ return nil, fmt.Errorf("no Packages in Document")
+ }
+ if len(doc.Packages) == 1 {
+ // get first (only) one and return its ID
+ for _, pkg := range doc.Packages {
+ return []common.ElementID{pkg.PackageSPDXIdentifier}, nil
+ }
+ }
+
+ // two or more packages, so we need to go through the relationships,
+ // find DESCRIBES or DESCRIBED_BY for this DOCUMENT, verify they are
+ // valid IDs in this document's packages, and return them
+ if doc.Relationships == nil {
+ return nil, fmt.Errorf("multiple Packages in Document but Relationships slice is nil")
+ }
+
+ eIDs, err := FilterRelationships2_1(doc, func(relationship *v2_1.Relationship) *common.ElementID {
+ refDocument := common.MakeDocElementID("", "DOCUMENT")
+
+ if relationship.Relationship == "DESCRIBES" && relationship.RefA == refDocument {
+ return &relationship.RefB.ElementRefID
+ } else if relationship.Relationship == "DESCRIBED_BY" && relationship.RefB == refDocument {
+ return &relationship.RefA.ElementRefID
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if len(eIDs) == 0 {
+ return nil, fmt.Errorf("no DESCRIBES or DESCRIBED_BY relationships found for this Document")
+ }
+
+ eIDs = SortElementIDs(eIDs)
+
+ return eIDs, nil
+}
+
+// GetDescribedPackageIDs2_2 returns a slice of ElementIDs for all Packages
+// in this Document that it "describes," according to SPDX rules:
+// - If the document has only one Package, its ID is returned.
+// - If the document has 2+ Packages, it returns the IDs of those that have
+// a DESCRIBES (or DESCRIBED_BY) relationship to this DOCUMENT.
+func GetDescribedPackageIDs2_2(doc *v2_2.Document) ([]common.ElementID, error) {
+ // if nil Packages map or zero packages in it, return empty slice
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("Packages map is nil")
+ }
+ if len(doc.Packages) == 0 {
+ return nil, fmt.Errorf("no Packages in Document")
+ }
+ if len(doc.Packages) == 1 {
+ // get first (only) one and return its ID
+ for _, pkg := range doc.Packages {
+ return []common.ElementID{pkg.PackageSPDXIdentifier}, nil
+ }
+ }
+
+ // two or more packages, so we need to go through the relationships,
+ // find DESCRIBES or DESCRIBED_BY for this DOCUMENT, verify they are
+ // valid IDs in this document's packages, and return them
+ if doc.Relationships == nil {
+ return nil, fmt.Errorf("multiple Packages in Document but Relationships slice is nil")
+ }
+
+ eIDs, err := FilterRelationships2_2(doc, func(relationship *v2_2.Relationship) *common.ElementID {
+ refDocument := common.MakeDocElementID("", "DOCUMENT")
+
+ if relationship.Relationship == "DESCRIBES" && relationship.RefA == refDocument {
+ return &relationship.RefB.ElementRefID
+ } else if relationship.Relationship == "DESCRIBED_BY" && relationship.RefB == refDocument {
+ return &relationship.RefA.ElementRefID
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if len(eIDs) == 0 {
+ return nil, fmt.Errorf("no DESCRIBES or DESCRIBED_BY relationships found for this Document")
+ }
+
+ eIDs = SortElementIDs(eIDs)
+
+ return eIDs, nil
+}
+
+// GetDescribedPackageIDs2_3 returns a slice of ElementIDs for all Packages
+// in this Document that it "describes," according to SPDX rules:
+// - If the document has only one Package, its ID is returned.
+// - If the document has 2+ Packages, it returns the IDs of those that have
+// a DESCRIBES (or DESCRIBED_BY) relationship to this DOCUMENT.
+func GetDescribedPackageIDs2_3(doc *v2_3.Document) ([]common.ElementID, error) {
+ // if nil Packages map or zero packages in it, return empty slice
+ if doc.Packages == nil {
+ return nil, fmt.Errorf("Packages map is nil")
+ }
+ if len(doc.Packages) == 0 {
+ return nil, fmt.Errorf("no Packages in Document")
+ }
+ if len(doc.Packages) == 1 {
+ // get first (only) one and return its ID
+ for _, pkg := range doc.Packages {
+ return []common.ElementID{pkg.PackageSPDXIdentifier}, nil
+ }
+ }
+
+ // two or more packages, so we need to go through the relationships,
+ // find DESCRIBES or DESCRIBED_BY for this DOCUMENT, verify they are
+ // valid IDs in this document's packages, and return them
+ if doc.Relationships == nil {
+ return nil, fmt.Errorf("multiple Packages in Document but Relationships slice is nil")
+ }
+
+ eIDs, err := FilterRelationships2_3(doc, func(relationship *v2_3.Relationship) *common.ElementID {
+ refDocument := common.MakeDocElementID("", "DOCUMENT")
+
+ if relationship.Relationship == "DESCRIBES" && relationship.RefA == refDocument {
+ return &relationship.RefB.ElementRefID
+ } else if relationship.Relationship == "DESCRIBED_BY" && relationship.RefB == refDocument {
+ return &relationship.RefA.ElementRefID
+ }
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ if len(eIDs) == 0 {
+ return nil, fmt.Errorf("no DESCRIBES or DESCRIBED_BY relationships found for this Document")
+ }
+
+ eIDs = SortElementIDs(eIDs)
+
+ return eIDs, nil
+}
diff --git a/spdxlib/described_elements_test.go b/spdxlib/described_elements_test.go
new file mode 100644
index 0000000..7398ec9
--- /dev/null
+++ b/spdxlib/described_elements_test.go
@@ -0,0 +1,519 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdxlib
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 tests =====
+
+func Test2_1CanGetIDsOfDescribedPackages(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_1.Relationship{
+ &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_1(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should be three of the five IDs, returned in alphabetical order
+ if len(describedPkgIDs) != 3 {
+ t.Fatalf("expected %d packages, got %d", 3, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+ if describedPkgIDs[1] != common.ElementID("p4") {
+ t.Errorf("expected %v, got %v", common.ElementID("p4"), describedPkgIDs[1])
+ }
+ if describedPkgIDs[2] != common.ElementID("p5") {
+ t.Errorf("expected %v, got %v", common.ElementID("p5"), describedPkgIDs[2])
+ }
+}
+
+func Test2_1GetDescribedPackagesReturnsSinglePackageIfOnlyOne(t *testing.T) {
+ // set up document and one package, but no relationships
+ // b/c only one package
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_1(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should return the single package
+ if len(describedPkgIDs) != 1 {
+ t.Fatalf("expected %d package, got %d", 1, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+}
+
+func Test2_1FailsToGetDescribedPackagesIfMoreThanOneWithoutDescribesRelationship(t *testing.T) {
+ // set up document and multiple packages, but no DESCRIBES relationships
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_1.Relationship{
+ // different relationship
+ &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_1(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_1FailsToGetDescribedPackagesIfMoreThanOneWithNilRelationships(t *testing.T) {
+ // set up document and multiple packages, but no relationships slice
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_1(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_1FailsToGetDescribedPackagesIfZeroPackagesInMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{},
+ }
+
+ _, err := GetDescribedPackageIDs2_1(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_1FailsToGetDescribedPackagesIfNilMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ }
+
+ _, err := GetDescribedPackageIDs2_1(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.2 tests =====
+
+func Test2_2CanGetIDsOfDescribedPackages(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_2.Relationship{
+ &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_2(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should be three of the five IDs, returned in alphabetical order
+ if len(describedPkgIDs) != 3 {
+ t.Fatalf("expected %d packages, got %d", 3, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+ if describedPkgIDs[1] != common.ElementID("p4") {
+ t.Errorf("expected %v, got %v", common.ElementID("p4"), describedPkgIDs[1])
+ }
+ if describedPkgIDs[2] != common.ElementID("p5") {
+ t.Errorf("expected %v, got %v", common.ElementID("p5"), describedPkgIDs[2])
+ }
+}
+
+func Test2_2GetDescribedPackagesReturnsSinglePackageIfOnlyOne(t *testing.T) {
+ // set up document and one package, but no relationships
+ // b/c only one package
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_2(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should return the single package
+ if len(describedPkgIDs) != 1 {
+ t.Fatalf("expected %d package, got %d", 1, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+}
+
+func Test2_2FailsToGetDescribedPackagesIfMoreThanOneWithoutDescribesRelationship(t *testing.T) {
+ // set up document and multiple packages, but no DESCRIBES relationships
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_2.Relationship{
+ // different relationship
+ &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_2(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_2FailsToGetDescribedPackagesIfMoreThanOneWithNilRelationships(t *testing.T) {
+ // set up document and multiple packages, but no relationships slice
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_2(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_2FailsToGetDescribedPackagesIfZeroPackagesInMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{},
+ }
+
+ _, err := GetDescribedPackageIDs2_2(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_2FailsToGetDescribedPackagesIfNilMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ }
+
+ _, err := GetDescribedPackageIDs2_2(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.3 tests =====
+
+func Test2_3CanGetIDsOfDescribedPackages(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_3.Relationship{
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_3(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should be three of the five IDs, returned in alphabetical order
+ if len(describedPkgIDs) != 3 {
+ t.Fatalf("expected %d packages, got %d", 3, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+ if describedPkgIDs[1] != common.ElementID("p4") {
+ t.Errorf("expected %v, got %v", common.ElementID("p4"), describedPkgIDs[1])
+ }
+ if describedPkgIDs[2] != common.ElementID("p5") {
+ t.Errorf("expected %v, got %v", common.ElementID("p5"), describedPkgIDs[2])
+ }
+}
+
+func Test2_3GetDescribedPackagesReturnsSinglePackageIfOnlyOne(t *testing.T) {
+ // set up document and one package, but no relationships
+ // b/c only one package
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ },
+ }
+
+ // request IDs for DESCRIBES / DESCRIBED_BY relationships
+ describedPkgIDs, err := GetDescribedPackageIDs2_3(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ // should return the single package
+ if len(describedPkgIDs) != 1 {
+ t.Fatalf("expected %d package, got %d", 1, len(describedPkgIDs))
+ }
+ if describedPkgIDs[0] != common.ElementID("p1") {
+ t.Errorf("expected %v, got %v", common.ElementID("p1"), describedPkgIDs[0])
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfMoreThanOneWithoutDescribesRelationship(t *testing.T) {
+ // set up document and multiple packages, but no DESCRIBES relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_3.Relationship{
+ // different relationship
+ &v2_3.Relationship{
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfMoreThanOneWithNilRelationships(t *testing.T) {
+ // set up document and multiple packages, but no relationships slice
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ },
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfZeroPackagesInMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{},
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+func Test2_3FailsToGetDescribedPackagesIfNilMap(t *testing.T) {
+ // set up document but no packages
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ }
+
+ _, err := GetDescribedPackageIDs2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/spdxlib/documents.go b/spdxlib/documents.go
new file mode 100644
index 0000000..dc41803
--- /dev/null
+++ b/spdxlib/documents.go
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package spdxlib
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ValidateDocument2_1 returns an error if the Document is found to be invalid, or nil if the Document is valid.
+// Currently, this only verifies that all Element IDs mentioned in Relationships exist in the Document as either a
+// Package or an UnpackagedFile.
+func ValidateDocument2_1(doc *v2_1.Document) error {
+ // cache a map of valid package IDs for quick lookups
+ validElementIDs := make(map[common.ElementID]bool)
+ for _, docPackage := range doc.Packages {
+ validElementIDs[docPackage.PackageSPDXIdentifier] = true
+ }
+
+ for _, unpackagedFile := range doc.Files {
+ validElementIDs[unpackagedFile.FileSPDXIdentifier] = true
+ }
+
+ // add the Document element ID
+ validElementIDs[common.MakeDocElementID("", "DOCUMENT").ElementRefID] = true
+
+ for _, relationship := range doc.Relationships {
+ if !validElementIDs[relationship.RefA.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefA.ElementRefID))
+ }
+
+ if !validElementIDs[relationship.RefB.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefB.ElementRefID))
+ }
+ }
+
+ return nil
+}
+
+// ValidateDocument2_2 returns an error if the Document is found to be invalid, or nil if the Document is valid.
+// Currently, this only verifies that all Element IDs mentioned in Relationships exist in the Document as either a
+// Package or an UnpackagedFile.
+func ValidateDocument2_2(doc *v2_2.Document) error {
+ // cache a map of package IDs for quick lookups
+ validElementIDs := make(map[common.ElementID]bool)
+ for _, docPackage := range doc.Packages {
+ validElementIDs[docPackage.PackageSPDXIdentifier] = true
+ }
+
+ for _, unpackagedFile := range doc.Files {
+ validElementIDs[unpackagedFile.FileSPDXIdentifier] = true
+ }
+
+ // add the Document element ID
+ validElementIDs[common.MakeDocElementID("", "DOCUMENT").ElementRefID] = true
+
+ for _, relationship := range doc.Relationships {
+ if !validElementIDs[relationship.RefA.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefA.ElementRefID))
+ }
+
+ if !validElementIDs[relationship.RefB.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefB.ElementRefID))
+ }
+ }
+
+ return nil
+}
+
+// ValidateDocument2_3 returns an error if the Document is found to be invalid, or nil if the Document is valid.
+// Currently, this only verifies that all Element IDs mentioned in Relationships exist in the Document as either a
+// Package or an UnpackagedFile.
+func ValidateDocument2_3(doc *v2_3.Document) error {
+ // cache a map of package IDs for quick lookups
+ validElementIDs := make(map[common.ElementID]bool)
+ for _, docPackage := range doc.Packages {
+ validElementIDs[docPackage.PackageSPDXIdentifier] = true
+ }
+
+ for _, unpackagedFile := range doc.Files {
+ validElementIDs[unpackagedFile.FileSPDXIdentifier] = true
+ }
+
+ // add the Document element ID
+ validElementIDs[common.MakeDocElementID("", "DOCUMENT").ElementRefID] = true
+
+ for _, relationship := range doc.Relationships {
+ if !validElementIDs[relationship.RefA.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefA.ElementRefID))
+ }
+
+ if !validElementIDs[relationship.RefB.ElementRefID] {
+ return fmt.Errorf("%s used in relationship but no such package exists", string(relationship.RefB.ElementRefID))
+ }
+ }
+
+ return nil
+}
diff --git a/spdxlib/documents_test.go b/spdxlib/documents_test.go
new file mode 100644
index 0000000..3557d34
--- /dev/null
+++ b/spdxlib/documents_test.go
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdxlib
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 tests =====
+
+func Test2_1ValidDocumentPassesValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_1.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ {
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_1(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got: %s", err.Error())
+ }
+}
+
+func Test2_1InvalidDocumentFailsValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ },
+ Relationships: []*v2_1.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DESCRIBES",
+ },
+ // invalid ID p99
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p99"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_1(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.2 tests =====
+
+func Test2_2ValidDocumentPassesValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_2.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ {
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_2(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got: %s", err.Error())
+ }
+}
+
+func Test2_2InvalidDocumentFailsValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ },
+ Relationships: []*v2_2.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // invalid ID p99
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p99"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_2(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.3 tests =====
+
+func Test2_3ValidDocumentPassesValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ {
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_3(doc)
+ if err != nil {
+ t.Fatalf("expected nil error, got: %s", err.Error())
+ }
+}
+
+func Test2_3InvalidDocumentFailsValidation(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_3.Document{
+ SPDXVersion: "SPDX-2.3",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_3.CreationInfo{},
+ Packages: []*v2_3.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // invalid ID p99
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p99"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ err := ValidateDocument2_3(doc)
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/spdxlib/element_ids.go b/spdxlib/element_ids.go
new file mode 100644
index 0000000..d505d40
--- /dev/null
+++ b/spdxlib/element_ids.go
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdxlib
+
+import (
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+// SortElementIDs sorts and returns the given slice of ElementIDs
+func SortElementIDs(eIDs []common.ElementID) []common.ElementID {
+ sort.Slice(eIDs, func(i, j int) bool {
+ return eIDs[i] < eIDs[j]
+ })
+
+ return eIDs
+}
diff --git a/spdxlib/element_ids_test.go b/spdxlib/element_ids_test.go
new file mode 100644
index 0000000..83893ba
--- /dev/null
+++ b/spdxlib/element_ids_test.go
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdxlib
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+)
+
+func TestSortElementIDs(t *testing.T) {
+ eIDs := []common.ElementID{"def", "abc", "123"}
+ eIDs = SortElementIDs(eIDs)
+
+ if !reflect.DeepEqual(eIDs, []common.ElementID{"123", "abc", "def"}) {
+ t.Fatalf("expected sorted ElementIDs, got: %v", eIDs)
+ }
+}
diff --git a/spdxlib/relationships.go b/spdxlib/relationships.go
new file mode 100644
index 0000000..aa807d0
--- /dev/null
+++ b/spdxlib/relationships.go
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdxlib
+
+import (
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// FilterRelationships2_1 returns a slice of Element IDs returned by the given filter closure. The closure is passed
+// one relationship at a time, and it can return an ElementID or nil.
+func FilterRelationships2_1(doc *v2_1.Document, filter func(*v2_1.Relationship) *common.ElementID) ([]common.ElementID, error) {
+ elementIDs := []common.ElementID{}
+
+ for _, relationship := range doc.Relationships {
+ if id := filter(relationship); id != nil {
+ elementIDs = append(elementIDs, *id)
+ }
+ }
+
+ return elementIDs, nil
+}
+
+// FilterRelationships2_2 returns a slice of Element IDs returned by the given filter closure. The closure is passed
+// one relationship at a time, and it can return an ElementID or nil.
+func FilterRelationships2_2(doc *v2_2.Document, filter func(*v2_2.Relationship) *common.ElementID) ([]common.ElementID, error) {
+ elementIDs := []common.ElementID{}
+
+ for _, relationship := range doc.Relationships {
+ if id := filter(relationship); id != nil {
+ elementIDs = append(elementIDs, *id)
+ }
+ }
+
+ return elementIDs, nil
+}
+
+// FilterRelationships2_3 returns a slice of Element IDs returned by the given filter closure. The closure is passed
+// one relationship at a time, and it can return an ElementID or nil.
+func FilterRelationships2_3(doc *v2_3.Document, filter func(*v2_3.Relationship) *common.ElementID) ([]common.ElementID, error) {
+ elementIDs := []common.ElementID{}
+
+ for _, relationship := range doc.Relationships {
+ if id := filter(relationship); id != nil {
+ elementIDs = append(elementIDs, *id)
+ }
+ }
+
+ return elementIDs, nil
+}
diff --git a/spdxlib/relationships_test.go b/spdxlib/relationships_test.go
new file mode 100644
index 0000000..5772887
--- /dev/null
+++ b/spdxlib/relationships_test.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdxlib
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== 2.1 tests =====
+
+func Test2_1FilterForDependencies(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_1.CreationInfo{},
+ Packages: []*v2_1.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_1.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ {
+ RefA: common.MakeDocElementID("", "p3"),
+ RefB: common.MakeDocElementID("", "p4"),
+ Relationship: "DEPENDENCY_OF",
+ },
+ },
+ }
+
+ eIDs, err := FilterRelationships2_1(doc, func(relationship *v2_1.Relationship) *common.ElementID {
+ p1EID := common.MakeDocElementID("", "p1")
+ if relationship.Relationship == "DEPENDS_ON" && relationship.RefA == p1EID {
+ return &relationship.RefB.ElementRefID
+ } else if relationship.Relationship == "DEPENDENCY_OF" && relationship.RefB == p1EID {
+ return &relationship.RefA.ElementRefID
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("expected non-nil err, got: %s", err.Error())
+ }
+
+ if len(eIDs) != 1 {
+ t.Fatalf("expected 1 ElementID, got: %v", eIDs)
+ }
+
+ if eIDs[0] != common.MakeDocElementID("", "p2").ElementRefID {
+ t.Fatalf("received unexpected relationship: %v", eIDs[0])
+ }
+}
+
+// ===== 2.2 tests =====
+
+func Test2_2FindsDependsOnRelationships(t *testing.T) {
+ // set up document and some packages and relationships
+ doc := &v2_2.Document{
+ SPDXVersion: "SPDX-2.2",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ CreationInfo: &v2_2.CreationInfo{},
+ Packages: []*v2_2.Package{
+ {PackageName: "pkg1", PackageSPDXIdentifier: "p1"},
+ {PackageName: "pkg2", PackageSPDXIdentifier: "p2"},
+ {PackageName: "pkg3", PackageSPDXIdentifier: "p3"},
+ {PackageName: "pkg4", PackageSPDXIdentifier: "p4"},
+ {PackageName: "pkg5", PackageSPDXIdentifier: "p5"},
+ },
+ Relationships: []*v2_2.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p5"),
+ Relationship: "DESCRIBES",
+ },
+ // inverse relationship -- should also get detected
+ {
+ RefA: common.MakeDocElementID("", "p4"),
+ RefB: common.MakeDocElementID("", "DOCUMENT"),
+ Relationship: "DESCRIBED_BY",
+ },
+ // different relationship
+ {
+ RefA: common.MakeDocElementID("", "p1"),
+ RefB: common.MakeDocElementID("", "p2"),
+ Relationship: "DEPENDS_ON",
+ },
+ },
+ }
+
+ eIDs, err := FilterRelationships2_2(doc, func(relationship *v2_2.Relationship) *common.ElementID {
+ p1EID := common.MakeDocElementID("", "p1")
+ if relationship.Relationship == "DEPENDS_ON" && relationship.RefA == p1EID {
+ return &relationship.RefB.ElementRefID
+ } else if relationship.Relationship == "DEPENDENCY_OF" && relationship.RefB == p1EID {
+ return &relationship.RefA.ElementRefID
+ }
+
+ return nil
+ })
+ if err != nil {
+ t.Fatalf("expected non-nil err, got: %s", err.Error())
+ }
+
+ if len(eIDs) != 1 {
+ t.Fatalf("expected 1 ElementID, got: %v", eIDs)
+ }
+
+ if eIDs[0] != common.MakeDocElementID("", "p2").ElementRefID {
+ t.Fatalf("received unexpected relationship: %v", eIDs[0])
+ }
+}
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/testdata/project1/emptyfile.testdata.txt b/testdata/project1/emptyfile.testdata.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project1/emptyfile.testdata.txt
diff --git a/testdata/project1/file1.testdata.txt b/testdata/project1/file1.testdata.txt
new file mode 100644
index 0000000..a9ec36c
--- /dev/null
+++ b/testdata/project1/file1.testdata.txt
@@ -0,0 +1 @@
+arbitrary content for testing purposes
diff --git a/testdata/project1/file3.testdata.txt b/testdata/project1/file3.testdata.txt
new file mode 100644
index 0000000..2a75979
--- /dev/null
+++ b/testdata/project1/file3.testdata.txt
@@ -0,0 +1 @@
+This file contains some sort of documentation or whatever
diff --git a/testdata/project1/folder1/file4.testdata.txt b/testdata/project1/folder1/file4.testdata.txt
new file mode 100644
index 0000000..3cc937e
--- /dev/null
+++ b/testdata/project1/folder1/file4.testdata.txt
@@ -0,0 +1,2 @@
+blah blah blah
+this file has text in it
diff --git a/testdata/project1/lastfile.testdata.txt b/testdata/project1/lastfile.testdata.txt
new file mode 100644
index 0000000..5545f2c
--- /dev/null
+++ b/testdata/project1/lastfile.testdata.txt
@@ -0,0 +1,3 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+somebody was kind enough to stick an spdx short-form ID in here
diff --git a/testdata/project1/symbolic-link b/testdata/project1/symbolic-link
new file mode 120000
index 0000000..11a1795
--- /dev/null
+++ b/testdata/project1/symbolic-link
@@ -0,0 +1 @@
+file3.testdata.txt \ No newline at end of file
diff --git a/testdata/project2/folder/has-one-id.py b/testdata/project2/folder/has-one-id.py
new file mode 100644
index 0000000..17ee16a
--- /dev/null
+++ b/testdata/project2/folder/has-one-id.py
@@ -0,0 +1,5 @@
+# a comment in the python file
+# SPDX-License-Identifier: MIT
+
+if __name__ == "__main__":
+ print("Hello world!")
diff --git a/testdata/project2/folder/has-trailing-comment-marker.c b/testdata/project2/folder/has-trailing-comment-marker.c
new file mode 100644
index 0000000..a157b4c
--- /dev/null
+++ b/testdata/project2/folder/has-trailing-comment-marker.c
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+# include <stdio.h>
+
+int main(int argc, char *argv[]) {
+ printf("Hello world!\n");
+ return 0;
+}
diff --git a/testdata/project2/has-duplicate-ids.txt b/testdata/project2/has-duplicate-ids.txt
new file mode 100644
index 0000000..b2ef883
--- /dev/null
+++ b/testdata/project2/has-duplicate-ids.txt
@@ -0,0 +1,7 @@
+SPDX-License-Identifier: MIT
+
+hi
+
+SPDX-License-Identifier: MIT
+
+oops we repeated it
diff --git a/testdata/project2/has-id.txt b/testdata/project2/has-id.txt
new file mode 100644
index 0000000..5545f2c
--- /dev/null
+++ b/testdata/project2/has-id.txt
@@ -0,0 +1,3 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+somebody was kind enough to stick an spdx short-form ID in here
diff --git a/testdata/project2/has-multiple-ids.txt b/testdata/project2/has-multiple-ids.txt
new file mode 100644
index 0000000..6962993
--- /dev/null
+++ b/testdata/project2/has-multiple-ids.txt
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: (MIT AND BSD-3-Clause) OR ISC
+
+hey look it's a short-form ID
+
+// SPDX-License-Identifier: BSD-2-Clause
+
+well there's another one, I guess it applies to this content?
+
+// SPDX-License-Identifier: EPL-1.0+
+
+and another one, with a + this time
diff --git a/testdata/project2/no-id.txt b/testdata/project2/no-id.txt
new file mode 100644
index 0000000..41444ff
--- /dev/null
+++ b/testdata/project2/no-id.txt
@@ -0,0 +1 @@
+this file doesn't have any short-form IDs
diff --git a/testdata/project3/alsoEXCLUDEthis.txt b/testdata/project3/alsoEXCLUDEthis.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/alsoEXCLUDEthis.txt
diff --git a/testdata/project3/dontscan.txt b/testdata/project3/dontscan.txt
new file mode 100644
index 0000000..73ff8db
--- /dev/null
+++ b/testdata/project3/dontscan.txt
@@ -0,0 +1 @@
+SPDX-License-Identifier: OOPS
diff --git a/testdata/project3/excludedir/no.txt b/testdata/project3/excludedir/no.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/excludedir/no.txt
diff --git a/testdata/project3/ignoredir/notThisOne.txt b/testdata/project3/ignoredir/notThisOne.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/ignoredir/notThisOne.txt
diff --git a/testdata/project3/ignorefile.txt b/testdata/project3/ignorefile.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/ignorefile.txt
diff --git a/testdata/project3/keep.txt b/testdata/project3/keep.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/keep.txt
diff --git a/testdata/project3/keep/keep.txt b/testdata/project3/keep/keep.txt
new file mode 100644
index 0000000..59d7f40
--- /dev/null
+++ b/testdata/project3/keep/keep.txt
@@ -0,0 +1 @@
+SPDX-License-Identifier: MIT
diff --git a/testdata/project3/subdir/ignoredir/notThisOneEither.txt b/testdata/project3/subdir/ignoredir/notThisOneEither.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/subdir/ignoredir/notThisOneEither.txt
diff --git a/testdata/project3/subdir/ignorefile.txt b/testdata/project3/subdir/ignorefile.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testdata/project3/subdir/ignorefile.txt
diff --git a/testdata/project3/subdir/keep/dontscan.txt b/testdata/project3/subdir/keep/dontscan.txt
new file mode 100644
index 0000000..73ff8db
--- /dev/null
+++ b/testdata/project3/subdir/keep/dontscan.txt
@@ -0,0 +1 @@
+SPDX-License-Identifier: OOPS
diff --git a/testdata/project3/subdir/keep/keep.txt b/testdata/project3/subdir/keep/keep.txt
new file mode 100644
index 0000000..59d7f40
--- /dev/null
+++ b/testdata/project3/subdir/keep/keep.txt
@@ -0,0 +1 @@
+SPDX-License-Identifier: MIT
diff --git a/testdata/project4/has-id-to-ignore.txt b/testdata/project4/has-id-to-ignore.txt
new file mode 100644
index 0000000..bd63676
--- /dev/null
+++ b/testdata/project4/has-id-to-ignore.txt
@@ -0,0 +1,8 @@
+this file actually contains some code talking about IDs, and should be ignored
+
+if file.contains("SPDX-License-Identifier: GPL-2.0") {
+ // do something...
+
+}
+
+We don't want to pick up that line as the short-form ID for this file.
diff --git a/testdata/project4/has-id.txt b/testdata/project4/has-id.txt
new file mode 100644
index 0000000..5545f2c
--- /dev/null
+++ b/testdata/project4/has-id.txt
@@ -0,0 +1,3 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+somebody was kind enough to stick an spdx short-form ID in here
diff --git a/testdata/project4/has-mix-of-ids.txt b/testdata/project4/has-mix-of-ids.txt
new file mode 100644
index 0000000..b6aa5d1
--- /dev/null
+++ b/testdata/project4/has-mix-of-ids.txt
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+
+the short-form ID above is correct, and should be picked up.
+
+the following code is talking about IDs, and should be ignored
+
+if file.contains("SPDX-License-Identifier: GPL-2.0") {
+ // do something...
+
+}
+
+We don't want to pick up that line as the short-form ID for this file.
diff --git a/tvloader/parser2v1/parse_annotation.go b/tvloader/parser2v1/parse_annotation.go
new file mode 100644
index 0000000..ca2e850
--- /dev/null
+++ b/tvloader/parser2v1/parse_annotation.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+)
+
+func (parser *tvParser2_1) parsePairForAnnotation2_1(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/parser2v1/parse_annotation_test.go b/tvloader/parser2v1/parse_annotation_test.go
new file mode 100644
index 0000000..41fe6a5
--- /dev/null
+++ b/tvloader/parser2v1/parse_annotation_test.go
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Annotation section tests =====
+func TestParser2_1FailsIfAnnotationNotSet(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePairForAnnotation2_1("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromAnnotation2_1 without setting ann pointer")
+ }
+}
+
+func TestParser2_1FailsIfAnnotationTagUnknown(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_1("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // parse invalid tag, using parsePairForAnnotation2_1(
+ err = parser.parsePairForAnnotation2_1("blah", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfAnnotationFieldsWithoutAnnotation(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("AnnotationDate", "2018-09-15T17:25:00Z")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1 for AnnotationDate without Annotator first")
+ }
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1 for AnnotationType without Annotator first")
+ }
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1 for SPDXREF without Annotator first")
+ }
+ err = parser.parsePair2_1("AnnotationComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1 for AnnotationComment without Annotator first")
+ }
+}
+
+func TestParser2_1CanParseAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // Annotator without email address
+ err := parser.parsePair2_1("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)
+ }
+ 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_1("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_1("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_1("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_1("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_1FailsIfAnnotatorInvalid(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("Annotator", "John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfAnnotatorTypeInvalid(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("Annotator", "Human: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfAnnotationRefInvalid(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_1("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePair2_1("SPDXREF", "blah:other")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v1/parse_creation_info.go b/tvloader/parser2v1/parse_creation_info.go
new file mode 100644
index 0000000..d7ca687
--- /dev/null
+++ b/tvloader/parser2v1/parse_creation_info.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func (parser *tvParser2_1) parsePairFromCreationInfo2_1(tag string, value string) error {
+ // fail if not in Creation Info parser state
+ if parser.st != psCreationInfo2_1 {
+ return fmt.Errorf("got invalid state %v in parsePairFromCreationInfo2_1", 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_1.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_1 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.st = psPackage2_1
+ parser.pkg = &v2_1.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ return parser.parsePairFromPackage2_1(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_1
+ parser.pkg = nil
+ return parser.parsePairFromFile2_1(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_1
+ return parser.parsePairFromOtherLicense2_1(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_1
+ return parser.parsePairFromReview2_1(tag, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_1.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_1(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_1(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_1.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_1(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/parser2v1/parse_creation_info_test.go b/tvloader/parser2v1/parse_creation_info_test.go
new file mode 100644
index 0000000..867491a
--- /dev/null
+++ b/tvloader/parser2v1/parse_creation_info_test.go
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Parser creation info state change tests =====
+func TestParser2_1CIMovesToPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ pkgName := "testPkg"
+ err := parser.parsePair2_1("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_1)
+ }
+ // 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_1CIMovesToFileAfterParsingFileNameTagWithNoPackages(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_1)
+ }
+ // 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_1CIMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_1)
+ }
+}
+
+func TestParser2_1CIMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_1)
+ }
+}
+
+func TestParser2_1CIStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePair2_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+
+ err = parser.parsePair2_1("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+}
+
+func TestParser2_1CIStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePair2_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationComment", "i guess i had something to say about this spdx file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psCreationInfo2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_1)
+ }
+}
+
+func TestParser2_1FailsParsingCreationInfoWithInvalidState(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psPackage2_1,
+ }
+ err := parser.parsePairFromCreationInfo2_1("SPDXVersion", "SPDX-2.1")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+// ===== Creation Info section tests =====
+func TestParser2_1HasCreationInfoAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePairFromCreationInfo2_1("LicenseListVersion", "3.9")
+ if err != nil {
+ t.Errorf("got error when calling parsePairFromCreationInfo2_1: %v", err)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_1CanParseCreationInfoTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // License List Version
+ err := parser.parsePairFromCreationInfo2_1("LicenseListVersion", "2.2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.LicenseListVersion != "2.2" {
+ t.Errorf("got %v for LicenseListVersion", parser.doc.CreationInfo.LicenseListVersion)
+ }
+
+ // Creators: Persons
+ refPersons := []string{
+ "Person: Person A",
+ "Person: Person B",
+ }
+ err = parser.parsePairFromCreationInfo2_1("Creator", refPersons[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_1("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 Creators", parser.doc.CreationInfo.Creators)
+ }
+
+ // Creators: Organizations
+ refOrgs := []string{
+ "Organization: Organization A",
+ "Organization: Organization B",
+ }
+ err = parser.parsePairFromCreationInfo2_1("Creator", refOrgs[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_1("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_1("Creator", refTools[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_1("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_1("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_1("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_1InvalidCreatorTagsFail(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePairFromCreationInfo2_1("Creator", "blah: somebody")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+
+ err = parser.parsePairFromCreationInfo2_1("Creator", "Tool with no colons")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+}
+
+func TestParser2_1CreatorTagWithMultipleColonsPasses(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePairFromCreationInfo2_1("Creator", "Tool: tool1:2:3")
+ if err != nil {
+ t.Errorf("unexpected error from parsing valid Creator format")
+ }
+}
+
+func TestParser2_1CIUnknownTagFails(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePairFromCreationInfo2_1("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_1CICreatesRelationship(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePair2_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1CICreatesAnnotation(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ err := parser.parsePair2_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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/parser2v1/parse_file.go b/tvloader/parser2v1/parse_file.go
new file mode 100644
index 0000000..8ecc280
--- /dev/null
+++ b/tvloader/parser2v1/parse_file.go
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func (parser *tvParser2_1) parsePairFromFile2_1(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 a spdxId or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_1 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.file = &v2_1.File{}
+ parser.file.FileName = value
+ // tag for creating new package section and going back to parsing Package
+ case "PackageName":
+ // check if the previous file contained a spdxId or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_1 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.st = psPackage2_1
+ parser.file = nil
+ return parser.parsePairFromPackage2_1(tag, value)
+ // tag for going on to snippet section
+ case "SnippetSPDXID":
+ parser.st = psSnippet2_1
+ return parser.parsePairFromSnippet2_1(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_1
+ return parser.parsePairFromOtherLicense2_1(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_1.File{}
+ }
+ parser.doc.Files = append(parser.doc.Files, parser.file)
+ } else {
+ if parser.pkg.Files == nil {
+ parser.pkg.Files = []*v2_1.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_1.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)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_1.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_1(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_1(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_1.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_1
+ return parser.parsePairFromReview2_1(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in File section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v1/parse_file_test.go b/tvloader/parser2v1/parse_file_test.go
new file mode 100644
index 0000000..33f4ef2
--- /dev/null
+++ b/tvloader/parser2v1/parse_file_test.go
@@ -0,0 +1,938 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Parser file section state change tests =====
+func TestParser2_1FileStartsNewFileAfterParsingFileNameTag(t *testing.T) {
+ // create the first file
+ fileOldName := "f1.txt"
+
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("FileName", fileName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_1 {
+ t.Errorf("expected state to be %v, got %v", psFile2_1, 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_1("SPDXID", "SPDXRef-f2ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1FileAddsToPackageOrUnpackagedFiles(t *testing.T) {
+ // start with no packages
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // add a file and SPDX identifier
+ fileName := "f2.txt"
+ err := parser.parsePair2_1("FileName", fileName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ err = parser.parsePair2_1("SPDXID", "SPDXRef-f2ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1("PackageName", "package1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ err = parser.parsePair2_1("SPDXID", "SPDXRef-pkg1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ err = parser.parsePair2_1("FileName", "f3.txt")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ err = parser.parsePair2_1("SPDXID", "SPDXRef-f3ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1FileStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ // create the first file and package
+ p1Name := "package1"
+ f1Name := "f1.txt"
+
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: p1Name, PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("PackageName", p2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should go back to Package
+ if parser.st != psPackage2_1 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_1, 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_1FileMovesToSnippetAfterParsingSnippetSPDXIDTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("SnippetSPDXID", "SPDXRef-Test1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psSnippet2_1 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_1, 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_1FileMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, parser.st)
+ }
+}
+
+func TestParser2_1FileMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, parser.st)
+ }
+}
+
+func TestParser2_1FileStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psFile2_1 {
+ t.Errorf("expected state to be %v, got %v", psFile2_1, parser.st)
+ }
+
+ err = parser.parsePair2_1("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psFile2_1 {
+ t.Errorf("expected state to be %v, got %v", psFile2_1, parser.st)
+ }
+}
+
+func TestParser2_1FileStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psFile2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psFile2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psFile2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_1)
+ }
+
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psFile2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psFile2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_1)
+ }
+}
+
+// ===== File data section tests =====
+func TestParser2_1CanParseFileTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // File Name
+ err := parser.parsePairFromFile2_1("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_1("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_1("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_1("FileChecksum", sumSha1)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_1("FileChecksum", sumSha256)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_1("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_1("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_1("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_1("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_1("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_1("ArtifactOfProjectName", "project1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_1("ArtifactOfProjectHomePage", "http://example.com/1/")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_1("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_1("ArtifactOfProjectName", "project2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // Artifact set 3 -- just name and home page
+ err = parser.parsePairFromFile2_1("ArtifactOfProjectName", "project3")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_1("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_1("ArtifactOfProjectName", "project4")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_1("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_1("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_1("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_1("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_1("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))
+ }
+
+}
+
+func TestParser2_1FileCreatesRelationshipInDocument(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1FileCreatesAnnotationInDocument(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1FileUnknownTagFails(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestFileAOPPointerChangesAfterTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("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_1("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_1("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_1("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_1("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_1FailsIfInvalidSPDXIDInFileSection(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_1("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid SPDX Identifier
+ err = parser.parsePairFromFile2_1("SPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfInvalidChecksumFormatInFileSection(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_1("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid format for checksum line, missing colon
+ err = parser.parsePairFromFile2_1("FileChecksum", "SHA1 85ed0817af83a24ad8da68c2b5094de69833983c")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfUnknownChecksumTypeInFileSection(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_1("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // unknown checksum type
+ err = parser.parsePairFromFile2_1("FileChecksum", "Special: 85ed0817af83a24ad8da68c2b5094de69833983c")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfArtifactHomePageBeforeArtifactName(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_1("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // artifact home page appears before artifact name
+ err = parser.parsePairFromFile2_1("ArtifactOfProjectHomePage", "https://example.com")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfArtifactURIBeforeArtifactName(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_1("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // artifact home page appears before artifact name
+ err = parser.parsePairFromFile2_1("ArtifactOfProjectURI", "https://example.com")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FilesWithoutSpdxIdThrowError(t *testing.T) {
+ // case 1: The previous file (packaged or unpackaged) does not contain spdxID
+ parser1 := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ file: &v2_1.File{FileName: "FileName"},
+ }
+
+ err := parser1.parsePair2_1("FileName", "f2")
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+
+ // case 2: Invalid file with snippet
+ // Last unpackaged file before a snippet starts
+ sid1 := common.ElementID("s1")
+ fileName := "f2.txt"
+ parser2 := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ file: &v2_1.File{FileName: fileName},
+ }
+ err = parser2.parsePair2_1("SnippetSPDXID", string(sid1))
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+
+ // case 3 : Invalid File without snippets
+ // Last unpackaged file before a package starts
+ // Last file of a package and New package starts
+ parser3 := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ fileName = "f3.txt"
+ err = parser3.parsePair2_1("FileName", fileName)
+ if err != nil {
+ t.Errorf("%s", err)
+ }
+ err = parser3.parsePair2_1("PackageName", "p2")
+ if err == nil {
+ t.Errorf("files withoutSpdx Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v1/parse_other_license.go b/tvloader/parser2v1/parse_other_license.go
new file mode 100644
index 0000000..a2c3d00
--- /dev/null
+++ b/tvloader/parser2v1/parse_other_license.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func (parser *tvParser2_1) parsePairFromOtherLicense2_1(tag string, value string) error {
+ switch tag {
+ // tag for creating new other license section
+ case "LicenseID":
+ parser.otherLic = &v2_1.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_1.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_1(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_1(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_1.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_1
+ return parser.parsePairFromReview2_1(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in OtherLicense section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v1/parse_other_license_test.go b/tvloader/parser2v1/parse_other_license_test.go
new file mode 100644
index 0000000..b1b7471
--- /dev/null
+++ b/tvloader/parser2v1/parse_other_license_test.go
@@ -0,0 +1,339 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Parser other license section state change tests =====
+func TestParser2_1OLStartsNewOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ // create the first other license
+ olid1 := "LicenseRef-Lic11"
+ olname1 := "License 11"
+
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.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_1("LicenseID", olid2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, 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_1("LicenseName", olname2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should still be correct
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, 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_1OLMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, parser.st)
+ }
+}
+
+func TestParser2_1OtherLicenseStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.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_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, 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_1("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, parser.st)
+ }
+}
+
+func TestParser2_1OtherLicenseStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.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_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_1)
+ }
+
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_1)
+ }
+
+ // 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_1OLFailsAfterParsingOtherSectionTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.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_1("SPDXVersion", "SPDX-2.1")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+ err = parser.parsePair2_1("PackageName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+ err = parser.parsePair2_1("FileName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+}
+
+// ===== Other License data section tests =====
+func TestParser2_1CanParseOtherLicenseTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("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_1("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_1("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_1("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_1("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_1OLUnknownTagFails(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psOtherLicense2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.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_1("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
diff --git a/tvloader/parser2v1/parse_package.go b/tvloader/parser2v1/parse_package.go
new file mode 100644
index 0000000..7a3e2a8
--- /dev/null
+++ b/tvloader/parser2v1/parse_package.go
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func (parser *tvParser2_1) parsePairFromPackage2_1(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 spdxId or not
+ if parser.pkg != nil && parser.pkg.PackageSPDXIdentifier == nullSpdxElementId2_1 {
+ return fmt.Errorf("package with PackageName %s does not have SPDX identifier", parser.pkg.PackageName)
+ }
+ parser.pkg = &v2_1.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ }
+ parser.pkg.PackageName = value
+ // tag for going on to file section
+ case "FileName":
+ parser.st = psFile2_1
+ return parser.parsePairFromFile2_1(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_1
+ return parser.parsePairFromOtherLicense2_1(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_1.Package{}
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ case "PackageVersion":
+ parser.pkg.PackageVersion = value
+ case "PackageFileName":
+ parser.pkg.PackageFileName = value
+ case "PackageSupplier":
+ parser.pkg.PackageSupplier = &common.Supplier{}
+ if value == "NOASSERTION" {
+ parser.pkg.PackageSupplier.Supplier = value
+ break
+ }
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ switch subkey {
+ case "Person", "Organization":
+ parser.pkg.PackageSupplier.Supplier = subvalue
+ parser.pkg.PackageSupplier.SupplierType = subkey
+ default:
+ return fmt.Errorf("unrecognized PackageSupplier type %v", subkey)
+ }
+ case "PackageOriginator":
+ parser.pkg.PackageOriginator = &common.Originator{}
+ if value == "NOASSERTION" {
+ parser.pkg.PackageOriginator.Originator = value
+ break
+ }
+ subkey, subvalue, err := extractSubs(value)
+ if err != nil {
+ return err
+ }
+ switch subkey {
+ case "Person", "Organization":
+ parser.pkg.PackageOriginator.Originator = subvalue
+ parser.pkg.PackageOriginator.OriginatorType = subkey
+ default:
+ return fmt.Errorf("unrecognized PackageOriginator type %v", subkey)
+ }
+ 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 "ExternalRef":
+ parser.pkgExtRef = &v2_1.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_1.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_1(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_1(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_1.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_1
+ return parser.parsePairFromReview2_1(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/parser2v1/parse_package_test.go b/tvloader/parser2v1/parse_package_test.go
new file mode 100644
index 0000000..052f535
--- /dev/null
+++ b/tvloader/parser2v1/parse_package_test.go
@@ -0,0 +1,1097 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Parser package section state change tests =====
+func TestParser2_1PackageStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ // create the first package
+ pkgOldName := "p1"
+
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: pkgOldName, PackageSPDXIdentifier: "p1"},
+ }
+ pkgOld := parser.pkg
+ parser.doc.Packages = append(parser.doc.Packages, pkgOld)
+
+ // now add a new package
+ pkgName := "p2"
+ err := parser.parsePair2_1("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_1 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_1, 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_1PackageStartsNewPackageAfterParsingPackageNameTagWhileInUnpackaged(t *testing.T) {
+ // pkg is nil, so that Files appearing before the first PackageName tag
+ // are added to Files instead of Packages
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psFile2_1,
+ 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_1("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_1 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_1, 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_1PackageMovesToFileAfterParsingFileNameTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ pkgCurrent := parser.pkg
+
+ err := parser.parsePair2_1("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_1 {
+ t.Errorf("expected state to be %v, got %v", psFile2_1, 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_1PackageMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_1("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, parser.st)
+ }
+}
+
+func TestParser2_1PackageMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_1("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, parser.st)
+ }
+}
+
+func TestParser2_1PackageStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psPackage2_1 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_1, parser.st)
+ }
+
+ err = parser.parsePair2_1("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psPackage2_1 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_1, parser.st)
+ }
+}
+
+func TestParser2_1PackageStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psPackage2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psPackage2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psPackage2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_1)
+ }
+
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psPackage2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationComment", "i guess i had something to say about this package")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psPackage2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_1)
+ }
+}
+
+// ===== Package data section tests =====
+func TestParser2_1CanParsePackageTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.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_1("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_1("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_1("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_1("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_1("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_1("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_1("PackageChecksum", sumSha1)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_1("PackageChecksum", sumSha256)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_1("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 PackageChecksum SHA1, got %s", codeSha1, checksum.Value)
+ }
+ case common.SHA256:
+ if checksum.Value != codeSha256 {
+ t.Errorf("expected %s for PackageChecksum SHA256, got %s", codeSha256, checksum.Value)
+ }
+ case common.MD5:
+ if checksum.Value != codeMd5 {
+ t.Errorf("expected %s for PackageChecksum MD5, got %s", codeMd5, checksum.Value)
+ }
+ }
+ }
+
+ // Package Home Page
+ err = parser.parsePairFromPackage2_1("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_1("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_1("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_1("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_1("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_1("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_1("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_1("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_1("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_1("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 External References and Comments
+ ref1 := "SECURITY cpe23Type cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"
+ ref1Category := "SECURITY"
+ ref1Type := common.TypeSecurityCPE23Type
+ 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_1("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_1("ExternalRefComment", ref1Comment)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_1("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_1("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_1CanParsePackageSupplierPersonTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: Person
+ err := parser.parsePairFromPackage2_1("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_1CanParsePackageSupplierOrganizationTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: Organization
+ err := parser.parsePairFromPackage2_1("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_1CanParsePackageSupplierNOASSERTIONTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: NOASSERTION
+ err := parser.parsePairFromPackage2_1("PackageSupplier", "NOASSERTION")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.pkg.PackageSupplier.Supplier != "NOASSERTION" {
+ t.Errorf("got false for PackageSupplierNOASSERTION")
+ }
+}
+
+func TestParser2_1CanParsePackageOriginatorPersonTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: Person
+ err := parser.parsePairFromPackage2_1("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 PackageOriginatorPerson", parser.pkg.PackageOriginator.Originator)
+ }
+}
+
+func TestParser2_1CanParsePackageOriginatorOrganizationTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: Organization
+ err := parser.parsePairFromPackage2_1("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 PackageOriginatorOrganization", parser.pkg.PackageOriginator.Originator)
+ }
+}
+
+func TestParser2_1CanParsePackageOriginatorNOASSERTIONTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: NOASSERTION
+ err := parser.parsePairFromPackage2_1("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_1CanParsePackageVerificationCodeTagWithExcludes(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.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_1("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_1CanParsePackageVerificationCodeTagWithoutExcludes(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.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_1("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 TestPackageExternalRefPointerChangesAfterTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.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_1("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_1("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_1("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_1("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_1PackageCreatesRelationshipInDocument(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1PackageCreatesAnnotationInDocument(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %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_1PackageUnknownTagFails(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePairFromPackage2_1("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_1FailsIfInvalidSPDXIDInPackageSection(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid ID format
+ err = parser.parsePairFromPackage2_1("SPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfInvalidPackageSupplierFormat(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid supplier format
+ err = parser.parsePairFromPackage2_1("PackageSupplier", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfUnknownPackageSupplierType(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid supplier type
+ err = parser.parsePairFromPackage2_1("PackageSupplier", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfInvalidPackageOriginatorFormat(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid originator format
+ err = parser.parsePairFromPackage2_1("PackageOriginator", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfUnknownPackageOriginatorType(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid originator type
+ err = parser.parsePairFromPackage2_1("PackageOriginator", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1SetsFilesAnalyzedTagsCorrectly(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // set tag
+ err = parser.parsePairFromPackage2_1("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_1FailsIfInvalidPackageChecksumFormat(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid checksum format
+ err = parser.parsePairFromPackage2_1("PackageChecksum", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfInvalidPackageChecksumType(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid checksum type
+ err = parser.parsePairFromPackage2_1("PackageChecksum", "whoops: blah")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfInvalidExternalRefFormat(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid external ref format
+ err = parser.parsePairFromPackage2_1("ExternalRef", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfExternalRefCommentBeforeExternalRef(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_1("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // external ref comment before external ref
+ err = parser.parsePairFromPackage2_1("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 := common.TypeSecurityCPE23Type
+ 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 := common.TypeSecurityCPE23Type
+ 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_1PackageWithoutSpdxIdentifierThrowsError(t *testing.T) {
+ // More than one package, the previous package doesn't contain the SPDXID
+ pkgOldName := "p1"
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psPackage2_1,
+ pkg: &v2_1.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_1("PackageName", pkgName)
+ if err == nil {
+ t.Errorf("package without SPDX Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v1/parse_relationship.go b/tvloader/parser2v1/parse_relationship.go
new file mode 100644
index 0000000..2b9b8cc
--- /dev/null
+++ b/tvloader/parser2v1/parse_relationship.go
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+ "strings"
+)
+
+func (parser *tvParser2_1) parsePairForRelationship2_1(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])
+ bID, err := extractDocElementID(strings.TrimSpace(rp[2]))
+ 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/parser2v1/parse_relationship_test.go b/tvloader/parser2v1/parse_relationship_test.go
new file mode 100644
index 0000000..6d44388
--- /dev/null
+++ b/tvloader/parser2v1/parse_relationship_test.go
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Relationship section tests =====
+func TestParser2_1FailsIfRelationshipNotSet(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePairForRelationship2_1("Relationship", "SPDXRef-A CONTAINS SPDXRef-B")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromRelationship2_1 without setting rln pointer")
+ }
+}
+
+func TestParser2_1FailsIfRelationshipCommentWithoutRelationship(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+ err := parser.parsePair2_1("RelationshipComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1 for RelationshipComment without Relationship first")
+ }
+}
+
+func TestParser2_1CanParseRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // Relationship
+ err := parser.parsePair2_1("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_1("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_1InvalidRelationshipTagsNoValueFail(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // no items
+ parser.rln = nil
+ err := parser.parsePair2_1("Relationship", "")
+ if err == nil {
+ t.Errorf("expected error for empty items in relationship, got nil")
+ }
+}
+
+func TestParser2_1InvalidRelationshipTagsOneValueFail(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // one item
+ parser.rln = nil
+ err := parser.parsePair2_1("Relationship", "DESCRIBES")
+ if err == nil {
+ t.Errorf("expected error for only one item in relationship, got nil")
+ }
+}
+
+func TestParser2_1InvalidRelationshipTagsTwoValuesFail(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // two items
+ parser.rln = nil
+ err := parser.parsePair2_1("Relationship", "SPDXRef-DOCUMENT DESCRIBES")
+ if err == nil {
+ t.Errorf("expected error for only two items in relationship, got nil")
+ }
+}
+
+func TestParser2_1InvalidRelationshipTagsThreeValuesSucceed(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // three items but with interspersed additional whitespace
+ parser.rln = nil
+ err := parser.parsePair2_1("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_1InvalidRelationshipTagsFourValuesFail(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // four items
+ parser.rln = nil
+ err := parser.parsePair2_1("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_1InvalidRelationshipTagsInvalidRefIDs(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // four items
+ parser.rln = nil
+ err := parser.parsePair2_1("Relationship", "SPDXRef-a DESCRIBES b")
+ if err == nil {
+ t.Errorf("expected error for missing SPDXRef- prefix, got nil")
+ }
+
+ parser.rln = nil
+ err = parser.parsePair2_1("Relationship", "a DESCRIBES SPDXRef-b")
+ if err == nil {
+ t.Errorf("expected error for missing SPDXRef- prefix, got nil")
+ }
+}
+
+func TestParser2_1FailsToParseUnknownTagInRelationshipSection(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ }
+
+ // Relationship
+ err := parser.parsePair2_1("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_1("blah", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v1/parse_review.go b/tvloader/parser2v1/parse_review.go
new file mode 100644
index 0000000..b241a22
--- /dev/null
+++ b/tvloader/parser2v1/parse_review.go
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func (parser *tvParser2_1) parsePairFromReview2_1(tag string, value string) error {
+ switch tag {
+ // tag for creating new review section
+ case "Reviewer":
+ parser.rev = &v2_1.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_1.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_1(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_1(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_1.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Review section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v1/parse_review_test.go b/tvloader/parser2v1/parse_review_test.go
new file mode 100644
index 0000000..c3cea2d
--- /dev/null
+++ b/tvloader/parser2v1/parse_review_test.go
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Parser review section state change tests =====
+func TestParser2_1ReviewStartsNewReviewAfterParsingReviewerTag(t *testing.T) {
+ // create the first review
+ rev1 := "John Doe"
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("Reviewer", rp2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, 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_1ReviewStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, 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_1("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, parser.st)
+ }
+}
+
+func TestParser2_1ReviewStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_1)
+ }
+
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_1)
+ }
+
+ // 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_1ReviewFailsAfterParsingOtherSectionTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("SPDXVersion", "SPDX-2.1")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+ err = parser.parsePair2_1("PackageName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+ err = parser.parsePair2_1("FileName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+ err = parser.parsePair2_1("LicenseID", "LicenseRef-Lic22")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_1, got nil")
+ }
+}
+
+// ===== Review data section tests =====
+func TestParser2_1CanParseReviewTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("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_1("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_1CanParseReviewerPersonTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("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_1CanParseReviewerOrganizationTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("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_1CanParseReviewerToolTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("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_1FailsIfReviewerInvalidFormat(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ rev: &v2_1.Review{},
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_1("Reviewer", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfReviewerUnknownType(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ rev: &v2_1.Review{},
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_1("Reviewer", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1ReviewUnknownTagFails(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psReview2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_1.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_1("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
diff --git a/tvloader/parser2v1/parse_snippet.go b/tvloader/parser2v1/parse_snippet.go
new file mode 100644
index 0000000..ad4c74f
--- /dev/null
+++ b/tvloader/parser2v1/parse_snippet.go
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func (parser *tvParser2_1) parsePairFromSnippet2_1(tag string, value string) error {
+ switch tag {
+ // tag for creating new snippet section
+ case "SnippetSPDXID":
+ // check here whether the previous file contained an SPDX ID or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_1 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.snippet = &v2_1.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_1.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_1
+ parser.snippet = nil
+ return parser.parsePairFromFile2_1(tag, value)
+ // tag for creating new package section and going back to parsing Package
+ case "PackageName":
+ parser.st = psPackage2_1
+ parser.file = nil
+ parser.snippet = nil
+ return parser.parsePairFromPackage2_1(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_1
+ return parser.parsePairFromOtherLicense2_1(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
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_1.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_1(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_1(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_1.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_1(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_1
+ return parser.parsePairFromReview2_1(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Snippet section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v1/parse_snippet_test.go b/tvloader/parser2v1/parse_snippet_test.go
new file mode 100644
index 0000000..aa42c3c
--- /dev/null
+++ b/tvloader/parser2v1/parse_snippet_test.go
@@ -0,0 +1,607 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Parser snippet section state change tests =====
+func TestParser2_1SnippetStartsNewSnippetAfterParsingSnippetSPDXIDTag(t *testing.T) {
+ // create the first snippet
+ sid1 := common.ElementID("s1")
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("SnippetSPDXID", "SPDXRef-s2")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psSnippet2_1 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_1, 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_1SnippetStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("PackageName", p2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should go back to Package
+ if parser.st != psPackage2_1 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_1, 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_1SnippetMovesToFileAfterParsingFileNameTag(t *testing.T) {
+ f1Name := "f1.txt"
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("FileName", f2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_1 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_1, 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_1SnippetMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psOtherLicense2_1 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_1, parser.st)
+ }
+}
+
+func TestParser2_1SnippetMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psReview2_1 {
+ t.Errorf("expected state to be %v, got %v", psReview2_1, parser.st)
+ }
+}
+
+func TestParser2_1SnippetStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psSnippet2_1 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_1, 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_1("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psSnippet2_1 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_1, parser.st)
+ }
+}
+
+func TestParser2_1SnippetStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psSnippet2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psSnippet2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psSnippet2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_1)
+ }
+
+ err = parser.parsePair2_1("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psSnippet2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_1)
+ }
+
+ err = parser.parsePair2_1("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.st != psSnippet2_1 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_1)
+ }
+
+ // 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_1CanParseSnippetTags(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("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_1("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_1("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_1("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_1("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_1("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_1("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_1("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_1("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_1("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)
+ }
+}
+
+func TestParser2_1SnippetUnknownTagFails(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePairFromSnippet2_1("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_1FailsForInvalidSnippetSPDXID(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("SnippetSPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsForInvalidSnippetFromFileSPDXID(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid From File identifier
+ err = parser.parsePairFromSnippet2_1("SnippetFromFileSPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsForInvalidSnippetByteValues(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid byte formats and values
+ err = parser.parsePairFromSnippet2_1("SnippetByteRange", "200 210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_1("SnippetByteRange", "a:210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_1("SnippetByteRange", "200:a")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsForInvalidSnippetLineValues(t *testing.T) {
+ parser := tvParser2_1{
+ doc: &v2_1.Document{Packages: []*v2_1.Package{}},
+ st: psSnippet2_1,
+ pkg: &v2_1.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_1.File{}},
+ file: &v2_1.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_1.Snippet{}},
+ snippet: &v2_1.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_1("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid byte formats and values
+ err = parser.parsePairFromSnippet2_1("SnippetLineRange", "200 210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_1("SnippetLineRange", "a:210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_1("SnippetLineRange", "200:a")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FilesWithoutSpdxIdThrowErrorWithSnippets(t *testing.T) {
+ // Invalid file with snippet
+ // Last unpackaged file before the snippet starts
+ // Last file of a package and new snippet starts
+ fileName := "f2.txt"
+ sid1 := common.ElementID("s1")
+ parser2 := tvParser2_1{
+ doc: &v2_1.Document{},
+ st: psCreationInfo2_1,
+ file: &v2_1.File{FileName: fileName},
+ }
+ err := parser2.parsePair2_1("SnippetSPDXID", string(sid1))
+ if err == nil {
+ t.Errorf("files without SPDX Identifiers getting accepted")
+ }
+
+}
diff --git a/tvloader/parser2v1/parser.go b/tvloader/parser2v1/parser.go
new file mode 100644
index 0000000..c083fc9
--- /dev/null
+++ b/tvloader/parser2v1/parser.go
@@ -0,0 +1,103 @@
+// Package parser2v1 contains functions to read, load and parse
+// SPDX tag-value files.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "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_1.Document, error) {
+ parser := tvParser2_1{}
+ for _, tv := range tvs {
+ err := parser.parsePair2_1(tv.Tag, tv.Value)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_1 {
+ return nil, fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ if parser.pkg != nil && parser.pkg.PackageSPDXIdentifier == nullSpdxElementId2_1 {
+ return nil, fmt.Errorf("package with PackageName %s does not have SPDX identifier", parser.pkg.PackageName)
+ }
+
+ return parser.doc, nil
+}
+
+func (parser *tvParser2_1) parsePair2_1(tag string, value string) error {
+ switch parser.st {
+ case psStart2_1:
+ return parser.parsePairFromStart2_1(tag, value)
+ case psCreationInfo2_1:
+ return parser.parsePairFromCreationInfo2_1(tag, value)
+ case psPackage2_1:
+ return parser.parsePairFromPackage2_1(tag, value)
+ case psFile2_1:
+ return parser.parsePairFromFile2_1(tag, value)
+ case psSnippet2_1:
+ return parser.parsePairFromSnippet2_1(tag, value)
+ case psOtherLicense2_1:
+ return parser.parsePairFromOtherLicense2_1(tag, value)
+ case psReview2_1:
+ return parser.parsePairFromReview2_1(tag, value)
+ default:
+ return fmt.Errorf("parser state %v not recognized when parsing (%s, %s)", parser.st, tag, value)
+ }
+}
+
+func (parser *tvParser2_1) parsePairFromStart2_1(tag string, value string) error {
+ // fail if not in Start parser state
+ if parser.st != psStart2_1 {
+ return fmt.Errorf("got invalid state %v in parsePairFromStart2_1", parser.st)
+ }
+
+ // create an SPDX Document data struct if we don't have one already
+ if parser.doc == nil {
+ parser.doc = &v2_1.Document{
+ ExternalDocumentReferences: []v2_1.ExternalDocumentRef{},
+ }
+ }
+
+ switch tag {
+ 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_1.ExternalDocumentRef{
+ DocumentRefID: documentRefID,
+ URI: uri,
+ Checksum: common.Checksum{Algorithm: common.ChecksumAlgorithm(alg), Value: checksum},
+ }
+ parser.doc.ExternalDocumentReferences = append(parser.doc.ExternalDocumentReferences, edr)
+ case "DocumentComment":
+ parser.doc.DocumentComment = value
+ default:
+ // move to Creation Info parser state
+ parser.st = psCreationInfo2_1
+ return parser.parsePairFromCreationInfo2_1(tag, value)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v1/parser_test.go b/tvloader/parser2v1/parser_test.go
new file mode 100644
index 0000000..e895445
--- /dev/null
+++ b/tvloader/parser2v1/parser_test.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/tvloader/reader"
+)
+
+// ===== Parser exported entry point tests =====
+func TestParser2_1CanParseTagValues(t *testing.T) {
+ var tvPairs []reader.TagValuePair
+
+ // create some pairs
+ tvPair1 := reader.TagValuePair{Tag: "SPDXVersion", Value: "SPDX-2.1"}
+ 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.1" {
+ t.Errorf("expected SPDXVersion to be SPDX-2.1, 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_1InitCreatesResetStatus(t *testing.T) {
+ parser := tvParser2_1{}
+ if parser.st != psStart2_1 {
+ t.Errorf("parser did not begin in start state")
+ }
+ if parser.doc != nil {
+ t.Errorf("parser did not begin with nil document")
+ }
+}
+
+func TestParser2_1HasDocumentAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_1{}
+ err := parser.parsePair2_1("SPDXVersion", "SPDX-2.1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_1: %v", err)
+ }
+ if parser.doc == nil {
+ t.Errorf("doc is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_1StartFailsToParseIfInInvalidState(t *testing.T) {
+ parser := tvParser2_1{st: psReview2_1}
+ err := parser.parsePairFromStart2_1("SPDXVersion", "SPDX-2.1")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FilesWithoutSpdxIdThrowErrorAtCompleteParse(t *testing.T) {
+ // case: checks the last file
+ // Last unpackaged file no packages in doc
+ // Last file of last package in the doc
+ tvPairs := []reader.TagValuePair{
+ {Tag: "SPDXVersion", Value: "SPDX-2.1"},
+ {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_1PackageWithoutSpdxIdThrowErrorAtCompleteParse(t *testing.T) {
+ // case: Checks the last package
+ tvPairs := []reader.TagValuePair{
+ {Tag: "SPDXVersion", Value: "SPDX-2.1"},
+ {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/parser2v1/types.go b/tvloader/parser2v1/types.go
new file mode 100644
index 0000000..8f3cf23
--- /dev/null
+++ b/tvloader/parser2v1/types.go
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+import (
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+type tvParser2_1 struct {
+ // document into which data is being parsed
+ doc *v2_1.Document
+
+ // current parser state
+ st tvParserState2_1
+
+ // current SPDX item being filled in, if any
+ pkg *v2_1.Package
+ pkgExtRef *v2_1.PackageExternalReference
+ file *v2_1.File
+ fileAOP *v2_1.ArtifactOfProject
+ snippet *v2_1.Snippet
+ otherLic *v2_1.OtherLicense
+ rln *v2_1.Relationship
+ ann *v2_1.Annotation
+ rev *v2_1.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.1)
+type tvParserState2_1 int
+
+const (
+ // at beginning of document
+ psStart2_1 tvParserState2_1 = iota
+
+ // in document creation info section
+ psCreationInfo2_1
+
+ // in package data section
+ psPackage2_1
+
+ // in file data section (including "unpackaged" files)
+ psFile2_1
+
+ // in snippet data section (including "unpackaged" files)
+ psSnippet2_1
+
+ // in other license section
+ psOtherLicense2_1
+
+ // in review section
+ psReview2_1
+)
+
+const nullSpdxElementId2_1 = common.ElementID("")
diff --git a/tvloader/parser2v1/util.go b/tvloader/parser2v1/util.go
new file mode 100644
index 0000000..7cac43d
--- /dev/null
+++ b/tvloader/parser2v1/util.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v1
+
+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 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/parser2v1/util_test.go b/tvloader/parser2v1/util_test.go
new file mode 100644
index 0000000..349d84f
--- /dev/null
+++ b/tvloader/parser2v1/util_test.go
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v1
+
+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 emptyString for ElementRefID, got %v", tst, deID.ElementRefID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, deID.ElementRefID)
+ }
+ }
+}
+
+func TestCanExtractElementRefsOnlyFromID(t *testing.T) {
+ // test with valid ID in this document
+ helperForExtractElementID(t, "SPDXRef-file1", false, "file1")
+ // test with valid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-file2", true, "")
+ // test with invalid ID in this document
+ helperForExtractElementID(t, "a:SPDXRef-file1", true, "")
+ helperForExtractElementID(t, "file1", true, "")
+ helperForExtractElementID(t, "SPDXRef-", true, "")
+ helperForExtractElementID(t, "SPDXRef-file1:", true, "")
+ // test with invalid ID in another document
+ helperForExtractElementID(t, "DocumentRef-doc2", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:SPDXRef-", true, "")
+ helperForExtractElementID(t, "DocumentRef-doc2:a", true, "")
+ helperForExtractElementID(t, "DocumentRef-:", true, "")
+ helperForExtractElementID(t, "DocumentRef-:SPDXRef-file1", true, "")
+}
+
+func helperForExtractElementID(t *testing.T, tst string, wantErr bool, wantElt string) {
+ eID, err := extractElementID(tst)
+ if err != nil && wantErr == false {
+ t.Errorf("testing %v: expected nil error, got %v", tst, err)
+ }
+ if err == nil && wantErr == true {
+ t.Errorf("testing %v: expected non-nil error, got nil", tst)
+ }
+ if eID != common.ElementID(wantElt) {
+ if wantElt == "" {
+ t.Errorf("testing %v: want emptyString for ElementRefID, got %v", tst, eID)
+ } else {
+ t.Errorf("testing %v: want %v for ElementRefID, got %v", tst, wantElt, eID)
+ }
+ }
+}
diff --git a/tvloader/parser2v2/parse_annotation.go b/tvloader/parser2v2/parse_annotation.go
new file mode 100644
index 0000000..4c5188e
--- /dev/null
+++ b/tvloader/parser2v2/parse_annotation.go
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+)
+
+func (parser *tvParser2_2) parsePairForAnnotation2_2(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/parser2v2/parse_annotation_test.go b/tvloader/parser2v2/parse_annotation_test.go
new file mode 100644
index 0000000..c2ba487
--- /dev/null
+++ b/tvloader/parser2v2/parse_annotation_test.go
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Annotation section tests =====
+func TestParser2_2FailsIfAnnotationNotSet(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePairForAnnotation2_2("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromAnnotation2_2 without setting ann pointer")
+ }
+}
+
+func TestParser2_2FailsIfAnnotationTagUnknown(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_2("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // parse invalid tag, using parsePairForAnnotation2_2(
+ err = parser.parsePairForAnnotation2_2("blah", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfAnnotationFieldsWithoutAnnotation(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("AnnotationDate", "2018-09-15T17:25:00Z")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2 for AnnotationDate without Annotator first")
+ }
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2 for AnnotationType without Annotator first")
+ }
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2 for SPDXREF without Annotator first")
+ }
+ err = parser.parsePair2_2("AnnotationComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2 for AnnotationComment without Annotator first")
+ }
+}
+
+func TestParser2_2CanParseAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // Annotator without email address
+ err := parser.parsePair2_2("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_2("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_2("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_2("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_2("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_2FailsIfAnnotatorInvalid(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("Annotator", "John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfAnnotatorTypeInvalid(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("Annotator", "Human: John Doe (jdoe@example.com)")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfAnnotationRefInvalid(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ // start with valid annotator
+ err := parser.parsePair2_2("Annotator", "Person: John Doe (jdoe@example.com)")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePair2_2("SPDXREF", "blah:other")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v2/parse_creation_info.go b/tvloader/parser2v2/parse_creation_info.go
new file mode 100644
index 0000000..da258bd
--- /dev/null
+++ b/tvloader/parser2v2/parse_creation_info.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *tvParser2_2) parsePairFromCreationInfo2_2(tag string, value string) error {
+ // fail if not in Creation Info parser state
+ if parser.st != psCreationInfo2_2 {
+ return fmt.Errorf("got invalid state %v in parsePairFromCreationInfo2_2", 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_2.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_2 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.st = psPackage2_2
+ parser.pkg = &v2_2.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ return parser.parsePairFromPackage2_2(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_2
+ parser.pkg = nil
+ return parser.parsePairFromFile2_2(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_2
+ return parser.parsePairFromOtherLicense2_2(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_2
+ return parser.parsePairFromReview2_2(tag, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_2.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_2(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_2(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_2.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_2(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/parser2v2/parse_creation_info_test.go b/tvloader/parser2v2/parse_creation_info_test.go
new file mode 100644
index 0000000..dcf6098
--- /dev/null
+++ b/tvloader/parser2v2/parse_creation_info_test.go
@@ -0,0 +1,427 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Parser creation info state change tests =====
+func TestParser2_2CIMovesToPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ pkgName := "testPkg"
+ err := parser.parsePair2_2("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_2)
+ }
+ // 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_2CIMovesToFileAfterParsingFileNameTagWithNoPackages(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_2)
+ }
+ // 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_2CIMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_2)
+ }
+}
+
+func TestParser2_2CIMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_2)
+ }
+}
+
+func TestParser2_2CIStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePair2_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+
+ err = parser.parsePair2_2("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+}
+
+func TestParser2_2CIStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePair2_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationComment", "i guess i had something to say about this spdx file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psCreationInfo2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psCreationInfo2_2)
+ }
+}
+
+func TestParser2_2FailsParsingCreationInfoWithInvalidState(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psPackage2_2,
+ }
+ err := parser.parsePairFromCreationInfo2_2("SPDXVersion", "SPDX-2.2")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+// ===== Creation Info section tests =====
+func TestParser2_2HasCreationInfoAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePairFromCreationInfo2_2("LicenseListVersion", "3.9")
+ if err != nil {
+ t.Errorf("got error when calling parsePairFromCreationInfo2_2: %v", err)
+ }
+ if parser.doc.CreationInfo == nil {
+ t.Errorf("doc.CreationInfo is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_2CanParseCreationInfoTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // License List Version
+ err := parser.parsePairFromCreationInfo2_2("LicenseListVersion", "2.2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ if parser.doc.CreationInfo.LicenseListVersion != "2.2" {
+ t.Errorf("got %v for LicenseListVersion", parser.doc.CreationInfo.LicenseListVersion)
+ }
+
+ // Creators: Persons
+ refPersons := []string{
+ "Person: Person A",
+ "Person: Person B",
+ }
+ err = parser.parsePairFromCreationInfo2_2("Creator", refPersons[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_2("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_2("Creator", refOrgs[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_2("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_2("Creator", refTools[0])
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromCreationInfo2_2("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_2("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_2("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_2InvalidCreatorTagsFail(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePairFromCreationInfo2_2("Creator", "blah: somebody")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+
+ err = parser.parsePairFromCreationInfo2_2("Creator", "Tool with no colons")
+ if err == nil {
+ t.Errorf("expected error from parsing invalid Creator format, got nil")
+ }
+}
+
+func TestParser2_2CreatorTagWithMultipleColonsPasses(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePairFromCreationInfo2_2("Creator", "Tool: tool1:2:3")
+ if err != nil {
+ t.Errorf("unexpected error from parsing valid Creator format")
+ }
+}
+
+func TestParser2_2CIUnknownTagFails(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePairFromCreationInfo2_2("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_2CICreatesRelationship(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePair2_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2CICreatesAnnotation(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ err := parser.parsePair2_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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/parser2v2/parse_file.go b/tvloader/parser2v2/parse_file.go
new file mode 100644
index 0000000..1828bb0
--- /dev/null
+++ b/tvloader/parser2v2/parse_file.go
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *tvParser2_2) parsePairFromFile2_2(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_2 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.file = &v2_2.File{}
+ parser.file.FileName = value
+ // tag for creating new package section and going back to parsing Package
+ case "PackageName":
+ parser.st = psPackage2_2
+ // check if the previous file contained an spdx Id or not
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_2 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.file = nil
+ return parser.parsePairFromPackage2_2(tag, value)
+ // tag for going on to snippet section
+ case "SnippetSPDXID":
+ parser.st = psSnippet2_2
+ return parser.parsePairFromSnippet2_2(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_2
+ return parser.parsePairFromOtherLicense2_2(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_2.File{}
+ }
+ parser.doc.Files = append(parser.doc.Files, parser.file)
+ } else {
+ if parser.pkg.Files == nil {
+ parser.pkg.Files = []*v2_2.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.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6:
+ 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_2.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_2.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_2(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_2(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_2.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_2
+ return parser.parsePairFromReview2_2(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in File section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v2/parse_file_test.go b/tvloader/parser2v2/parse_file_test.go
new file mode 100644
index 0000000..0bcea7d
--- /dev/null
+++ b/tvloader/parser2v2/parse_file_test.go
@@ -0,0 +1,954 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Parser file section state change tests =====
+func TestParser2_2FileStartsNewFileAfterParsingFileNameTag(t *testing.T) {
+ // create the first file
+ fileOldName := "f1.txt"
+
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("FileName", fileName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_2 {
+ t.Errorf("expected state to be %v, got %v", psFile2_2, 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_2("SPDXID", "SPDXRef-f2ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2FileAddsToPackageOrUnpackagedFiles(t *testing.T) {
+ // start with no packages
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // add a file and SPDX identifier
+ fileName := "f2.txt"
+ err := parser.parsePair2_2("FileName", fileName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ err = parser.parsePair2_2("SPDXID", "SPDXRef-f2ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2("PackageName", "package1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ err = parser.parsePair2_2("SPDXID", "SPDXRef-pkg1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ err = parser.parsePair2_2("FileName", "f3.txt")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ err = parser.parsePair2_2("SPDXID", "SPDXRef-f3ID")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2FileStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ // create the first file and package
+ p1Name := "package1"
+ f1Name := "f1.txt"
+
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: p1Name, PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("PackageName", p2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should go back to Package
+ if parser.st != psPackage2_2 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_2, 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_2FileMovesToSnippetAfterParsingSnippetSPDXIDTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("SnippetSPDXID", "SPDXRef-Test1")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psSnippet2_2 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_2, 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_2FileMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, parser.st)
+ }
+}
+
+func TestParser2_2FileMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, parser.st)
+ }
+}
+
+func TestParser2_2FileStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psFile2_2 {
+ t.Errorf("expected state to be %v, got %v", psFile2_2, parser.st)
+ }
+
+ err = parser.parsePair2_2("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psFile2_2 {
+ t.Errorf("expected state to be %v, got %v", psFile2_2, parser.st)
+ }
+}
+
+func TestParser2_2FileStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psFile2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psFile2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psFile2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_2)
+ }
+
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psFile2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psFile2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psFile2_2)
+ }
+}
+
+// ===== File data section tests =====
+func TestParser2_2CanParseFileTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // File Name
+ err := parser.parsePairFromFile2_2("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_2("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_2("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))
+ }
+
+ testChecksums := map[common.ChecksumAlgorithm]string{
+ "MD5": "624c1abb3664f4b35547e7c73864ad24",
+ "SHA1": "85ed0817af83a24ad8da68c2b5094de69833983c",
+ "SHA256": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ "SHA512": "4ced3267f5ed38df65ceebc43e97aa6c2948cc7ef3288c2e5074e7df7fab544cc93339604513ea5f65616f9ed1c48581465043c8a9b693ef20fd4fddaf25e1b9",
+ }
+
+ for algo, tc := range testChecksums {
+ if err := parser.parsePairFromFile2_2(
+ "FileChecksum", fmt.Sprintf("%s: %s", algo, tc)); err != nil {
+ t.Errorf("expected error, got %v", err)
+ }
+ }
+
+ for _, checksum := range parser.file.Checksums {
+ if checksum.Value != testChecksums[checksum.Algorithm] {
+ t.Errorf(
+ "expected %s for FileChecksum%s, got %s",
+ testChecksums[checksum.Algorithm], checksum.Algorithm, checksum.Value,
+ )
+ }
+ }
+
+ // Concluded License
+ err = parser.parsePairFromFile2_2("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_2("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_2("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_2("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_2("ArtifactOfProjectName", "project1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_2("ArtifactOfProjectHomePage", "http://example.com/1/")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_2("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_2("ArtifactOfProjectName", "project2")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // Artifact set 3 -- just name and home page
+ err = parser.parsePairFromFile2_2("ArtifactOfProjectName", "project3")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_2("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_2("ArtifactOfProjectName", "project4")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromFile2_2("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_2("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_2("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_2("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_2("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_2("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_2FileCreatesRelationshipInDocument(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2FileCreatesAnnotationInDocument(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2FileUnknownTagFails(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestFileAOPPointerChangesAfterTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("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_2("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_2("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_2("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_2("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_2FailsIfInvalidSPDXIDInFileSection(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_2("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid SPDX Identifier
+ err = parser.parsePairFromFile2_2("SPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfInvalidChecksumFormatInFileSection(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_2("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid format for checksum line, missing colon
+ err = parser.parsePairFromFile2_2("FileChecksum", "SHA1 85ed0817af83a24ad8da68c2b5094de69833983c")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_1FailsIfUnknownChecksumTypeInFileSection(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_2("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // unknown checksum type
+ err = parser.parsePairFromFile2_2("FileChecksum", "Special: 85ed0817af83a24ad8da68c2b5094de69833983c")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfArtifactHomePageBeforeArtifactName(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_2("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // artifact home page appears before artifact name
+ err = parser.parsePairFromFile2_2("ArtifactOfProjectHomePage", "https://example.com")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfArtifactURIBeforeArtifactName(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // start with File Name
+ err := parser.parsePairFromFile2_2("FileName", "f1.txt")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // artifact home page appears before artifact name
+ err = parser.parsePairFromFile2_2("ArtifactOfProjectURI", "https://example.com")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FilesWithoutSpdxIdThrowError(t *testing.T) {
+ // case 1: The previous file (packaged or unpackaged) does not contain spdx ID
+ parser1 := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ file: &v2_2.File{FileName: "FileName"},
+ }
+
+ err := parser1.parsePair2_2("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_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ file: &v2_2.File{FileName: fileName},
+ }
+ err = parser2.parsePair2_2("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_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ fileName = "f3.txt"
+ err = parser3.parsePair2_2("FileName", fileName)
+ if err != nil {
+ t.Errorf("%s", err)
+ }
+ err = parser3.parsePair2_2("PackageName", "p2")
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v2/parse_other_license.go b/tvloader/parser2v2/parse_other_license.go
new file mode 100644
index 0000000..95250a6
--- /dev/null
+++ b/tvloader/parser2v2/parse_other_license.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *tvParser2_2) parsePairFromOtherLicense2_2(tag string, value string) error {
+ switch tag {
+ // tag for creating new other license section
+ case "LicenseID":
+ parser.otherLic = &v2_2.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_2.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_2(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_2(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_2.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_2
+ return parser.parsePairFromReview2_2(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in OtherLicense section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v2/parse_other_license_test.go b/tvloader/parser2v2/parse_other_license_test.go
new file mode 100644
index 0000000..cdba840
--- /dev/null
+++ b/tvloader/parser2v2/parse_other_license_test.go
@@ -0,0 +1,339 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Parser other license section state change tests =====
+func TestParser2_2OLStartsNewOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ // create the first other license
+ olid1 := "LicenseRef-Lic11"
+ olname1 := "License 11"
+
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.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_2("LicenseID", olid2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, 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_2("LicenseName", olname2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should still be correct
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, 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_2OLMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, parser.st)
+ }
+}
+
+func TestParser2_2OtherLicenseStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.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_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, 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_2("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, parser.st)
+ }
+}
+
+func TestParser2_2OtherLicenseStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.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_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_2)
+ }
+
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psOtherLicense2_2)
+ }
+
+ // 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_2OLFailsAfterParsingOtherSectionTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.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_2("SPDXVersion", "SPDX-2.2")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+ err = parser.parsePair2_2("PackageName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+ err = parser.parsePair2_2("FileName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+}
+
+// ===== Other License data section tests =====
+func TestParser2_2CanParseOtherLicenseTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("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_2("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_2("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_2("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_2("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_2OLUnknownTagFails(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psOtherLicense2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.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_2("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
diff --git a/tvloader/parser2v2/parse_package.go b/tvloader/parser2v2/parse_package.go
new file mode 100644
index 0000000..dbea6df
--- /dev/null
+++ b/tvloader/parser2v2/parse_package.go
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *tvParser2_2) parsePairFromPackage2_2(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_2 {
+ return fmt.Errorf("package with PackageName %s does not have SPDX identifier", parser.pkg.PackageName)
+ }
+ parser.pkg = &v2_2.Package{
+ FilesAnalyzed: true,
+ IsFilesAnalyzedTagPresent: false,
+ }
+ }
+ parser.pkg.PackageName = value
+ // tag for going on to file section
+ case "FileName":
+ parser.st = psFile2_2
+ return parser.parsePairFromFile2_2(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_2
+ return parser.parsePairFromOtherLicense2_2(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_2.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.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6:
+ 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 "PackageAttributionText":
+ parser.pkg.PackageAttributionTexts = append(parser.pkg.PackageAttributionTexts, value)
+ case "ExternalRef":
+ parser.pkgExtRef = &v2_2.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_2.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_2(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_2(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_2.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_2
+ return parser.parsePairFromReview2_2(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/parser2v2/parse_package_test.go b/tvloader/parser2v2/parse_package_test.go
new file mode 100644
index 0000000..e07cf5a
--- /dev/null
+++ b/tvloader/parser2v2/parse_package_test.go
@@ -0,0 +1,1118 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Parser package section state change tests =====
+func TestParser2_2PackageStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ // create the first package
+ pkgOldName := "p1"
+
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.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_2("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_2 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_2, 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_2PackageStartsNewPackageAfterParsingPackageNameTagWhileInUnpackaged(t *testing.T) {
+ // pkg is nil, so that Files appearing before the first PackageName tag
+ // are added to Files instead of Packages
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psFile2_2,
+ 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_2("PackageName", pkgName)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psPackage2_2 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_2, 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_2PackageMovesToFileAfterParsingFileNameTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ pkgCurrent := parser.pkg
+
+ err := parser.parsePair2_2("FileName", "testFile")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_2 {
+ t.Errorf("expected state to be %v, got %v", psFile2_2, 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_2PackageMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_2("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, parser.st)
+ }
+}
+
+func TestParser2_2PackageMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_2("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, parser.st)
+ }
+}
+
+func TestParser2_2PackageStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psPackage2_2 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_2, parser.st)
+ }
+
+ err = parser.parsePair2_2("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psPackage2_2 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_2, parser.st)
+ }
+}
+
+func TestParser2_2PackageStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psPackage2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psPackage2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psPackage2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_2)
+ }
+
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psPackage2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationComment", "i guess i had something to say about this package")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psPackage2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psPackage2_2)
+ }
+}
+
+// ===== Package data section tests =====
+func TestParser2_2CanParsePackageTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.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_2("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_2("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_2("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_2("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_2("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_2("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
+
+ testChecksums := map[common.ChecksumAlgorithm]string{
+ "MD5": "624c1abb3664f4b35547e7c73864ad24",
+ "SHA1": "85ed0817af83a24ad8da68c2b5094de69833983c",
+ "SHA256": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ "SHA512": "4ced3267f5ed38df65ceebc43e97aa6c2948cc7ef3288c2e5074e7df7fab544cc93339604513ea5f65616f9ed1c48581465043c8a9b693ef20fd4fddaf25e1b9",
+ }
+
+ for algo, tc := range testChecksums {
+ if err := parser.parsePairFromPackage2_2(
+ "PackageChecksum", fmt.Sprintf("%s: %s", algo, tc)); err != nil {
+ t.Errorf("expected error, got %v", err)
+ }
+ }
+
+ for _, checksum := range parser.pkg.PackageChecksums {
+ if checksum.Value != testChecksums[checksum.Algorithm] {
+ t.Errorf(
+ "expected %s for PackageChecksum%s, got %s",
+ testChecksums[checksum.Algorithm], checksum.Algorithm, checksum.Value,
+ )
+ }
+ }
+
+ // Package Home Page
+ err = parser.parsePairFromPackage2_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("ExternalRefComment", ref1Comment)
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ err = parser.parsePairFromPackage2_2("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_2("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_2CanParsePackageSupplierPersonTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: Person
+ err := parser.parsePairFromPackage2_2("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_2CanParsePackageSupplierOrganizationTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: Organization
+ err := parser.parsePairFromPackage2_2("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_2CanParsePackageSupplierNOASSERTIONTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Supplier: NOASSERTION
+ err := parser.parsePairFromPackage2_2("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_2CanParsePackageOriginatorPersonTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: Person
+ err := parser.parsePairFromPackage2_2("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_2CanParsePackageOriginatorOrganizationTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: Organization
+ err := parser.parsePairFromPackage2_2("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_2CanParsePackageOriginatorNOASSERTIONTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ // Package Originator: NOASSERTION
+ err := parser.parsePairFromPackage2_2("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_2CanParsePackageVerificationCodeTagWithExcludes(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.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_2("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_2CanParsePackageVerificationCodeTagWithoutExcludes(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.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_2("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_2PackageExternalRefPointerChangesAfterTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.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_2("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_2("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_2("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_2("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_2PackageCreatesRelationshipInDocument(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-whatever")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2PackageCreatesAnnotationInDocument(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePair2_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %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_2PackageUnknownTagFails(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{PackageName: "p1", PackageSPDXIdentifier: "p1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+
+ err := parser.parsePairFromPackage2_2("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_2FailsIfInvalidSPDXIDInPackageSection(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid ID format
+ err = parser.parsePairFromPackage2_2("SPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfInvalidPackageSupplierFormat(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid supplier format
+ err = parser.parsePairFromPackage2_2("PackageSupplier", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfUnknownPackageSupplierType(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid supplier type
+ err = parser.parsePairFromPackage2_2("PackageSupplier", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfInvalidPackageOriginatorFormat(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid originator format
+ err = parser.parsePairFromPackage2_2("PackageOriginator", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfUnknownPackageOriginatorType(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid originator type
+ err = parser.parsePairFromPackage2_2("PackageOriginator", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2SetsFilesAnalyzedTagsCorrectly(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // set tag
+ err = parser.parsePairFromPackage2_2("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_2FailsIfInvalidPackageChecksumFormat(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid checksum format
+ err = parser.parsePairFromPackage2_2("PackageChecksum", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfInvalidPackageChecksumType(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid checksum type
+ err = parser.parsePairFromPackage2_2("PackageChecksum", "whoops: blah")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfInvalidExternalRefFormat(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid external ref format
+ err = parser.parsePairFromPackage2_2("ExternalRef", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfExternalRefCommentBeforeExternalRef(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.Package{},
+ }
+
+ // start with Package Name
+ err := parser.parsePairFromPackage2_2("PackageName", "p1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // external ref comment before external ref
+ err = parser.parsePairFromPackage2_2("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_2PackageWithoutSpdxIdentifierThrowsError(t *testing.T) {
+ // More than one package, the previous package doesn't contain an SPDX ID
+ pkgOldName := "p1"
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psPackage2_2,
+ pkg: &v2_2.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_2("PackageName", pkgName)
+ if err == nil {
+ t.Errorf("package without SPDX Identifier getting accepted")
+ }
+}
diff --git a/tvloader/parser2v2/parse_relationship.go b/tvloader/parser2v2/parse_relationship.go
new file mode 100644
index 0000000..092b554
--- /dev/null
+++ b/tvloader/parser2v2/parse_relationship.go
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strings"
+)
+
+func (parser *tvParser2_2) parsePairForRelationship2_2(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/parser2v2/parse_relationship_test.go b/tvloader/parser2v2/parse_relationship_test.go
new file mode 100644
index 0000000..48285fc
--- /dev/null
+++ b/tvloader/parser2v2/parse_relationship_test.go
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Relationship section tests =====
+func TestParser2_2FailsIfRelationshipNotSet(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePairForRelationship2_2("Relationship", "SPDXRef-A CONTAINS SPDXRef-B")
+ if err == nil {
+ t.Errorf("expected error when calling parsePairFromRelationship2_2 without setting rln pointer")
+ }
+}
+
+func TestParser2_2FailsIfRelationshipCommentWithoutRelationship(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+ err := parser.parsePair2_2("RelationshipComment", "comment whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2 for RelationshipComment without Relationship first")
+ }
+}
+
+func TestParser2_2CanParseRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // Relationship
+ err := parser.parsePair2_2("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_2("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_2InvalidRelationshipTagsNoValueFail(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // no items
+ parser.rln = nil
+ err := parser.parsePair2_2("Relationship", "")
+ if err == nil {
+ t.Errorf("expected error for empty items in relationship, got nil")
+ }
+}
+
+func TestParser2_2InvalidRelationshipTagsOneValueFail(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // one item
+ parser.rln = nil
+ err := parser.parsePair2_2("Relationship", "DESCRIBES")
+ if err == nil {
+ t.Errorf("expected error for only one item in relationship, got nil")
+ }
+}
+
+func TestParser2_2InvalidRelationshipTagsTwoValuesFail(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // two items
+ parser.rln = nil
+ err := parser.parsePair2_2("Relationship", "SPDXRef-DOCUMENT DESCRIBES")
+ if err == nil {
+ t.Errorf("expected error for only two items in relationship, got nil")
+ }
+}
+
+func TestParser2_2InvalidRelationshipTagsThreeValuesSucceed(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // three items but with interspersed additional whitespace
+ parser.rln = nil
+ err := parser.parsePair2_2("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_2InvalidRelationshipTagsFourValuesFail(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // four items
+ parser.rln = nil
+ err := parser.parsePair2_2("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_2InvalidRelationshipTagsInvalidRefIDs(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // four items
+ parser.rln = nil
+ err := parser.parsePair2_2("Relationship", "SPDXRef-a DESCRIBES b")
+ if err == nil {
+ t.Errorf("expected error for missing SPDXRef- prefix, got nil")
+ }
+
+ parser.rln = nil
+ err = parser.parsePair2_2("Relationship", "a DESCRIBES SPDXRef-b")
+ if err == nil {
+ t.Errorf("expected error for missing SPDXRef- prefix, got nil")
+ }
+}
+
+func TestParser2_2SpecialValuesValidForRightSideOfRelationship(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // NONE in right side of relationship should pass
+ err := parser.parsePair2_2("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_2("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_2("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_2("Relationship", "NOASSERTION CONTAINS SPDXRef-a")
+ if err == nil {
+ t.Errorf("expected non-nil error for NOASSERTION CONTAINS, got nil")
+ }
+}
+
+func TestParser2_2FailsToParseUnknownTagInRelationshipSection(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ }
+
+ // Relationship
+ err := parser.parsePair2_2("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_2("blah", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
diff --git a/tvloader/parser2v2/parse_review.go b/tvloader/parser2v2/parse_review.go
new file mode 100644
index 0000000..09691c8
--- /dev/null
+++ b/tvloader/parser2v2/parse_review.go
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *tvParser2_2) parsePairFromReview2_2(tag string, value string) error {
+ switch tag {
+ // tag for creating new review section
+ case "Reviewer":
+ parser.rev = &v2_2.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_2.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_2(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_2(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_2.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Review section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v2/parse_review_test.go b/tvloader/parser2v2/parse_review_test.go
new file mode 100644
index 0000000..f93afbf
--- /dev/null
+++ b/tvloader/parser2v2/parse_review_test.go
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Parser review section state change tests =====
+func TestParser2_2ReviewStartsNewReviewAfterParsingReviewerTag(t *testing.T) {
+ // create the first review
+ rev1 := "John Doe"
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("Reviewer", rp2)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, 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_2ReviewStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, 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_2("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, parser.st)
+ }
+}
+
+func TestParser2_2ReviewStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_2)
+ }
+
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psReview2_2)
+ }
+
+ // 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_2ReviewFailsAfterParsingOtherSectionTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("SPDXVersion", "SPDX-2.2")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+ err = parser.parsePair2_2("PackageName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+ err = parser.parsePair2_2("FileName", "whatever")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+ err = parser.parsePair2_2("LicenseID", "LicenseRef-Lic22")
+ if err == nil {
+ t.Errorf("expected error when calling parsePair2_2, got nil")
+ }
+}
+
+// ===== Review data section tests =====
+func TestParser2_2CanParseReviewTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("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_2("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_2CanParseReviewerPersonTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("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_2CanParseReviewerOrganizationTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("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_2CanParseReviewerToolTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("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_2FailsIfReviewerInvalidFormat(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ rev: &v2_2.Review{},
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_2("Reviewer", "oops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsIfReviewerUnknownType(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ rev: &v2_2.Review{},
+ }
+ parser.doc.Reviews = append(parser.doc.Reviews, parser.rev)
+
+ err := parser.parsePairFromReview2_2("Reviewer", "whoops: John Doe")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2ReviewUnknownTagFails(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psReview2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1"},
+ otherLic: &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-Lic11",
+ LicenseName: "License 11",
+ },
+ rev: &v2_2.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_2("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
diff --git a/tvloader/parser2v2/parse_snippet.go b/tvloader/parser2v2/parse_snippet.go
new file mode 100644
index 0000000..5b4f41a
--- /dev/null
+++ b/tvloader/parser2v2/parse_snippet.go
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func (parser *tvParser2_2) parsePairFromSnippet2_2(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_2 {
+ return fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ parser.snippet = &v2_2.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_2.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_2
+ parser.snippet = nil
+ return parser.parsePairFromFile2_2(tag, value)
+ // tag for creating new package section and going back to parsing Package
+ case "PackageName":
+ parser.st = psPackage2_2
+ parser.file = nil
+ parser.snippet = nil
+ return parser.parsePairFromPackage2_2(tag, value)
+ // tag for going on to other license section
+ case "LicenseID":
+ parser.st = psOtherLicense2_2
+ return parser.parsePairFromOtherLicense2_2(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_2.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_2(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_2(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_2.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_2(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_2
+ return parser.parsePairFromReview2_2(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in Snippet section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v2/parse_snippet_test.go b/tvloader/parser2v2/parse_snippet_test.go
new file mode 100644
index 0000000..1d6ddb2
--- /dev/null
+++ b/tvloader/parser2v2/parse_snippet_test.go
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Parser snippet section state change tests =====
+func TestParser2_2SnippetStartsNewSnippetAfterParsingSnippetSPDXIDTag(t *testing.T) {
+ // create the first snippet
+ sid1 := common.ElementID("s1")
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "test", PackageSPDXIdentifier: "test", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("SnippetSPDXID", "SPDXRef-s2")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psSnippet2_2 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_2, 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_2SnippetStartsNewPackageAfterParsingPackageNameTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("PackageName", p2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should go back to Package
+ if parser.st != psPackage2_2 {
+ t.Errorf("expected state to be %v, got %v", psPackage2_2, 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_2SnippetMovesToFileAfterParsingFileNameTag(t *testing.T) {
+ f1Name := "f1.txt"
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("FileName", f2Name)
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should be correct
+ if parser.st != psFile2_2 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_2, 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_2SnippetMovesToOtherLicenseAfterParsingLicenseIDTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("LicenseID", "LicenseRef-TestLic")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psOtherLicense2_2 {
+ t.Errorf("expected state to be %v, got %v", psOtherLicense2_2, parser.st)
+ }
+}
+
+func TestParser2_2SnippetMovesToReviewAfterParsingReviewerTag(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("Reviewer", "Person: John Doe")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psReview2_2 {
+ t.Errorf("expected state to be %v, got %v", psReview2_2, parser.st)
+ }
+}
+
+func TestParser2_2SnippetStaysAfterParsingRelationshipTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("Relationship", "SPDXRef-blah CONTAINS SPDXRef-blah-else")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should remain unchanged
+ if parser.st != psSnippet2_2 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_2, 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_2("RelationshipComment", "blah")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ // state should still remain unchanged
+ if parser.st != psSnippet2_2 {
+ t.Errorf("expected state to be %v, got %v", psSnippet2_2, parser.st)
+ }
+}
+
+func TestParser2_2SnippetStaysAfterParsingAnnotationTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("Annotator", "Person: John Doe ()")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psSnippet2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationDate", "2018-09-15T00:36:00Z")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psSnippet2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationType", "REVIEW")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psSnippet2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_2)
+ }
+
+ err = parser.parsePair2_2("SPDXREF", "SPDXRef-45")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psSnippet2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_2)
+ }
+
+ err = parser.parsePair2_2("AnnotationComment", "i guess i had something to say about this particular file")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.st != psSnippet2_2 {
+ t.Errorf("parser is in state %v, expected %v", parser.st, psSnippet2_2)
+ }
+
+ // 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_2CanParseSnippetTags(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2("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_2SnippetUnknownTagFails(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.Snippet{SnippetSPDXIdentifier: "s1"},
+ }
+ parser.doc.Packages = append(parser.doc.Packages, parser.pkg)
+ parser.pkg.Files = append(parser.pkg.Files, parser.file)
+
+ err := parser.parsePairFromSnippet2_2("blah", "something")
+ if err == nil {
+ t.Errorf("expected error from parsing unknown tag")
+ }
+}
+
+func TestParser2_2FailsForInvalidSnippetSPDXID(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("SnippetSPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsForInvalidSnippetFromFileSPDXID(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid From File identifier
+ err = parser.parsePairFromSnippet2_2("SnippetFromFileSPDXID", "whoops")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsForInvalidSnippetByteValues(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid byte formats and values
+ err = parser.parsePairFromSnippet2_2("SnippetByteRange", "200 210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_2("SnippetByteRange", "a:210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_2("SnippetByteRange", "200:a")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FailsForInvalidSnippetLineValues(t *testing.T) {
+ parser := tvParser2_2{
+ doc: &v2_2.Document{Packages: []*v2_2.Package{}},
+ st: psSnippet2_2,
+ pkg: &v2_2.Package{PackageName: "package1", PackageSPDXIdentifier: "package1", Files: []*v2_2.File{}},
+ file: &v2_2.File{FileName: "f1.txt", FileSPDXIdentifier: "f1", Snippets: map[common.ElementID]*v2_2.Snippet{}},
+ snippet: &v2_2.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_2("SnippetSPDXID", "SPDXRef-s1")
+ if err != nil {
+ t.Errorf("expected nil error, got %v", err)
+ }
+ // invalid byte formats and values
+ err = parser.parsePairFromSnippet2_2("SnippetLineRange", "200 210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_2("SnippetLineRange", "a:210")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+ err = parser.parsePairFromSnippet2_2("SnippetLineRange", "200:a")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FilesWithoutSpdxIdThrowErrorWithSnippets(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_2{
+ doc: &v2_2.Document{},
+ st: psCreationInfo2_2,
+ file: &v2_2.File{FileName: fileName},
+ }
+ err := parser2.parsePair2_2("SnippetSPDXID", string(sid1))
+ if err == nil {
+ t.Errorf("file without SPDX Identifier getting accepted")
+ }
+
+}
diff --git a/tvloader/parser2v2/parser.go b/tvloader/parser2v2/parser.go
new file mode 100644
index 0000000..72f67be
--- /dev/null
+++ b/tvloader/parser2v2/parser.go
@@ -0,0 +1,100 @@
+// Package parser2v2 contains functions to read, load and parse
+// SPDX tag-value files, version 2.2.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "fmt"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "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_2.Document, error) {
+ parser := tvParser2_2{}
+ for _, tv := range tvs {
+ err := parser.parsePair2_2(tv.Tag, tv.Value)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if parser.file != nil && parser.file.FileSPDXIdentifier == nullSpdxElementId2_2 {
+ return nil, fmt.Errorf("file with FileName %s does not have SPDX identifier", parser.file.FileName)
+ }
+ if parser.pkg != nil && parser.pkg.PackageSPDXIdentifier == nullSpdxElementId2_2 {
+ return nil, fmt.Errorf("package with PackageName %s does not have SPDX identifier", parser.pkg.PackageName)
+ }
+ return parser.doc, nil
+}
+
+func (parser *tvParser2_2) parsePair2_2(tag string, value string) error {
+ switch parser.st {
+ case psStart2_2:
+ return parser.parsePairFromStart2_2(tag, value)
+ case psCreationInfo2_2:
+ return parser.parsePairFromCreationInfo2_2(tag, value)
+ case psPackage2_2:
+ return parser.parsePairFromPackage2_2(tag, value)
+ case psFile2_2:
+ return parser.parsePairFromFile2_2(tag, value)
+ case psSnippet2_2:
+ return parser.parsePairFromSnippet2_2(tag, value)
+ case psOtherLicense2_2:
+ return parser.parsePairFromOtherLicense2_2(tag, value)
+ case psReview2_2:
+ return parser.parsePairFromReview2_2(tag, value)
+ default:
+ return fmt.Errorf("parser state %v not recognized when parsing (%s, %s)", parser.st, tag, value)
+ }
+}
+
+func (parser *tvParser2_2) parsePairFromStart2_2(tag string, value string) error {
+ // fail if not in Start parser state
+ if parser.st != psStart2_2 {
+ return fmt.Errorf("got invalid state %v in parsePairFromStart2_2", parser.st)
+ }
+
+ // create an SPDX Document data struct if we don't have one already
+ if parser.doc == nil {
+ parser.doc = &v2_2.Document{ExternalDocumentReferences: []v2_2.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_2.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_2
+ return parser.parsePairFromCreationInfo2_2(tag, value)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v2/parser_test.go b/tvloader/parser2v2/parser_test.go
new file mode 100644
index 0000000..148264d
--- /dev/null
+++ b/tvloader/parser2v2/parser_test.go
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/tvloader/reader"
+)
+
+// ===== Parser exported entry point tests =====
+func TestParser2_2CanParseTagValues(t *testing.T) {
+ var tvPairs []reader.TagValuePair
+
+ // create some pairs
+ tvPair1 := reader.TagValuePair{Tag: "SPDXVersion", Value: "SPDX-2.2"}
+ 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.2" {
+ t.Errorf("expected SPDXVersion to be SPDX-2.2, 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_2InitCreatesResetStatus(t *testing.T) {
+ parser := tvParser2_2{}
+ if parser.st != psStart2_2 {
+ t.Errorf("parser did not begin in start state")
+ }
+ if parser.doc != nil {
+ t.Errorf("parser did not begin with nil document")
+ }
+}
+
+func TestParser2_2HasDocumentAfterCallToParseFirstTag(t *testing.T) {
+ parser := tvParser2_2{}
+ err := parser.parsePair2_2("SPDXVersion", "SPDX-2.2")
+ if err != nil {
+ t.Errorf("got error when calling parsePair2_2: %v", err)
+ }
+ if parser.doc == nil {
+ t.Errorf("doc is still nil after parsing first pair")
+ }
+}
+
+func TestParser2_2StartFailsToParseIfInInvalidState(t *testing.T) {
+ parser := tvParser2_2{st: psReview2_2}
+ err := parser.parsePairFromStart2_2("SPDXVersion", "SPDX-2.2")
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestParser2_2FilesWithoutSpdxIdThrowErrorAtCompleteParse(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.2"},
+ {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_2PackageWithoutSpdxIdThrowErrorAtCompleteParse(t *testing.T) {
+ // case: Checks the last package
+ tvPairs := []reader.TagValuePair{
+ {Tag: "SPDXVersion", Value: "SPDX-2.2"},
+ {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/parser2v2/types.go b/tvloader/parser2v2/types.go
new file mode 100644
index 0000000..52d6a9a
--- /dev/null
+++ b/tvloader/parser2v2/types.go
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+import (
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+type tvParser2_2 struct {
+ // document into which data is being parsed
+ doc *v2_2.Document
+
+ // current parser state
+ st tvParserState2_2
+
+ // current SPDX item being filled in, if any
+ pkg *v2_2.Package
+ pkgExtRef *v2_2.PackageExternalReference
+ file *v2_2.File
+ fileAOP *v2_2.ArtifactOfProject
+ snippet *v2_2.Snippet
+ otherLic *v2_2.OtherLicense
+ rln *v2_2.Relationship
+ ann *v2_2.Annotation
+ rev *v2_2.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.2)
+type tvParserState2_2 int
+
+const (
+ // at beginning of document
+ psStart2_2 tvParserState2_2 = iota
+
+ // in document creation info section
+ psCreationInfo2_2
+
+ // in package data section
+ psPackage2_2
+
+ // in file data section (including "unpackaged" files)
+ psFile2_2
+
+ // in snippet data section (including "unpackaged" files)
+ psSnippet2_2
+
+ // in other license section
+ psOtherLicense2_2
+
+ // in review section
+ psReview2_2
+)
+
+const nullSpdxElementId2_2 = common.ElementID("")
diff --git a/tvloader/parser2v2/util.go b/tvloader/parser2v2/util.go
new file mode 100644
index 0000000..5e84433
--- /dev/null
+++ b/tvloader/parser2v2/util.go
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package parser2v2
+
+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.2 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/parser2v2/util_test.go b/tvloader/parser2v2/util_test.go
new file mode 100644
index 0000000..80050f3
--- /dev/null
+++ b/tvloader/parser2v2/util_test.go
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v2
+
+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/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..e502648
--- /dev/null
+++ b/tvloader/parser2v3/parse_file.go
@@ -0,0 +1,157 @@
+// 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.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6,
+ common.SHA3_256,
+ common.SHA3_384,
+ common.SHA3_512,
+ common.BLAKE2b_256,
+ common.BLAKE2b_384,
+ common.BLAKE2b_512,
+ common.BLAKE3,
+ common.ADLER32:
+ algorithm := common.ChecksumAlgorithm(subkey)
+ parser.file.Checksums = append(parser.file.Checksums, common.Checksum{Algorithm: algorithm, Value: subvalue})
+ default:
+ return fmt.Errorf("got unknown checksum type %s", subkey)
+ }
+ case "LicenseConcluded":
+ parser.file.LicenseConcluded = value
+ case "LicenseInfoInFile":
+ parser.file.LicenseInfoInFiles = append(parser.file.LicenseInfoInFiles, value)
+ case "LicenseComments":
+ parser.file.LicenseComments = value
+ case "FileCopyrightText":
+ parser.file.FileCopyrightText = value
+ case "ArtifactOfProjectName":
+ parser.fileAOP = &v2_3.ArtifactOfProject{}
+ parser.file.ArtifactOfProjects = append(parser.file.ArtifactOfProjects, parser.fileAOP)
+ parser.fileAOP.Name = value
+ case "ArtifactOfProjectHomePage":
+ if parser.fileAOP == nil {
+ return fmt.Errorf("no current ArtifactOfProject found")
+ }
+ parser.fileAOP.HomePage = value
+ case "ArtifactOfProjectURI":
+ if parser.fileAOP == nil {
+ return fmt.Errorf("no current ArtifactOfProject found")
+ }
+ parser.fileAOP.URI = value
+ case "FileComment":
+ parser.file.FileComment = value
+ case "FileNotice":
+ parser.file.FileNotice = value
+ case "FileContributor":
+ parser.file.FileContributors = append(parser.file.FileContributors, value)
+ case "FileDependency":
+ parser.file.FileDependencies = append(parser.file.FileDependencies, value)
+ case "FileAttributionText":
+ parser.file.FileAttributionTexts = append(parser.file.FileAttributionTexts, value)
+ // for relationship tags, pass along but don't change state
+ case "Relationship":
+ parser.rln = &v2_3.Relationship{}
+ parser.doc.Relationships = append(parser.doc.Relationships, parser.rln)
+ return parser.parsePairForRelationship2_3(tag, value)
+ case "RelationshipComment":
+ return parser.parsePairForRelationship2_3(tag, value)
+ // for annotation tags, pass along but don't change state
+ case "Annotator":
+ parser.ann = &v2_3.Annotation{}
+ parser.doc.Annotations = append(parser.doc.Annotations, parser.ann)
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationDate":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationType":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "SPDXREF":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ case "AnnotationComment":
+ return parser.parsePairForAnnotation2_3(tag, value)
+ // tag for going on to review section (DEPRECATED)
+ case "Reviewer":
+ parser.st = psReview2_3
+ return parser.parsePairFromReview2_3(tag, value)
+ default:
+ return fmt.Errorf("received unknown tag %v in File section", tag)
+ }
+
+ return nil
+}
diff --git a/tvloader/parser2v3/parse_file_test.go b/tvloader/parser2v3/parse_file_test.go
new file mode 100644
index 0000000..f158baf
--- /dev/null
+++ b/tvloader/parser2v3/parse_file_test.go
@@ -0,0 +1,955 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "fmt"
+ "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))
+ }
+
+ testChecksums := map[common.ChecksumAlgorithm]string{
+ "MD5": "624c1abb3664f4b35547e7c73864ad24",
+ "SHA1": "85ed0817af83a24ad8da68c2b5094de69833983c",
+ "SHA256": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ "SHA512": "4ced3267f5ed38df65ceebc43e97aa6c2948cc7ef3288c2e5074e7df7fab544cc93339604513ea5f65616f9ed1c48581465043c8a9b693ef20fd4fddaf25e1b9",
+ "BLAKE3": "981d32ed7aad9e408c5c36f6346c915ba11c2bd8b3e7d44902a11d7a141abdd9",
+ }
+
+ for algo, tc := range testChecksums {
+ if err := parser.parsePairFromFile2_3(
+ "FileChecksum", fmt.Sprintf("%s: %s", algo, tc)); err != nil {
+ t.Errorf("expected error, got %v", err)
+ }
+ }
+
+ for _, checksum := range parser.file.Checksums {
+ if checksum.Value != testChecksums[checksum.Algorithm] {
+ t.Errorf(
+ "expected %s for FileChecksum%s, got %s",
+ testChecksums[checksum.Algorithm], checksum.Algorithm, 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..989261d
--- /dev/null
+++ b/tvloader/parser2v3/parse_package.go
@@ -0,0 +1,248 @@
+// 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.SHA224,
+ common.SHA256,
+ common.SHA384,
+ common.SHA512,
+ common.MD2,
+ common.MD4,
+ common.MD5,
+ common.MD6,
+ common.SHA3_256,
+ common.SHA3_384,
+ common.SHA3_512,
+ common.BLAKE2b_256,
+ common.BLAKE2b_384,
+ common.BLAKE2b_512,
+ common.BLAKE3,
+ common.ADLER32:
+ algorithm := common.ChecksumAlgorithm(subkey)
+ parser.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..c5f09af
--- /dev/null
+++ b/tvloader/parser2v3/parse_package_test.go
@@ -0,0 +1,1119 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package parser2v3
+
+import (
+ "fmt"
+ "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
+
+ testChecksums := map[common.ChecksumAlgorithm]string{
+ "MD5": "624c1abb3664f4b35547e7c73864ad24",
+ "SHA1": "85ed0817af83a24ad8da68c2b5094de69833983c",
+ "SHA256": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ "SHA512": "4ced3267f5ed38df65ceebc43e97aa6c2948cc7ef3288c2e5074e7df7fab544cc93339604513ea5f65616f9ed1c48581465043c8a9b693ef20fd4fddaf25e1b9",
+ "BLAKE3": "981d32ed7aad9e408c5c36f6346c915ba11c2bd8b3e7d44902a11d7a141abdd9",
+ }
+
+ for algo, tc := range testChecksums {
+ if err := parser.parsePairFromPackage2_3(
+ "PackageChecksum", fmt.Sprintf("%s: %s", algo, tc)); err != nil {
+ t.Errorf("expected error, got %v", err)
+ }
+ }
+
+ for _, checksum := range parser.pkg.PackageChecksums {
+ if checksum.Value != testChecksums[checksum.Algorithm] {
+ t.Errorf(
+ "expected %s for PackageChecksum%s, got %s",
+ testChecksums[checksum.Algorithm], checksum.Algorithm, 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/reader/reader.go b/tvloader/reader/reader.go
new file mode 100644
index 0000000..786f7ea
--- /dev/null
+++ b/tvloader/reader/reader.go
@@ -0,0 +1,161 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package reader
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "strings"
+ "unicode"
+)
+
+// TagValuePair is a convenience struct for a (tag, value) string pair.
+type TagValuePair struct {
+ Tag string
+ Value string
+}
+
+// ReadTagValues takes an io.Reader, scans it line by line and returns
+// a slice of {string, string} structs in the form {tag, value}.
+func ReadTagValues(content io.Reader) ([]TagValuePair, error) {
+ r := &tvReader{}
+
+ scanner := bufio.NewScanner(content)
+ for scanner.Scan() {
+ // read each line, one by one
+ err := r.readNextLine(scanner.Text())
+ if err != nil {
+ return nil, err
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ // finalize and make sure all is well
+ tvList, err := r.finalize()
+ if err != nil {
+ return nil, err
+ }
+
+ // convert internal format to exported TagValueList
+ var exportedTVList []TagValuePair
+ for _, tv := range tvList {
+ tvPair := TagValuePair{Tag: tv.tag, Value: tv.value}
+ exportedTVList = append(exportedTVList, tvPair)
+ }
+
+ return exportedTVList, nil
+}
+
+type tagvalue struct {
+ tag string
+ value string
+}
+
+type tvReader struct {
+ midtext bool
+ tvList []tagvalue
+ currentLine int
+ currentTag string
+ currentValue string
+}
+
+func (reader *tvReader) finalize() ([]tagvalue, error) {
+ if reader.midtext {
+ return nil, fmt.Errorf("finalize called while still midtext parsing a text tag")
+ }
+ return reader.tvList, nil
+}
+
+func (reader *tvReader) readNextLine(line string) error {
+ reader.currentLine++
+
+ if reader.midtext {
+ return reader.readNextLineFromMidtext(line)
+ }
+
+ return reader.readNextLineFromReady(line)
+}
+
+func (reader *tvReader) readNextLineFromReady(line string) error {
+ // strip whitespace from beginning of line
+ line2 := strings.TrimLeftFunc(line, func(r rune) bool {
+ return unicode.IsSpace(r)
+ })
+
+ // ignore empty lines
+ if line2 == "" {
+ return nil
+ }
+
+ // ignore comment lines
+ if strings.HasPrefix(line2, "#") {
+ return nil
+ }
+
+ // split at colon
+ substrings := strings.SplitN(line2, ":", 2)
+ if len(substrings) == 1 {
+ // error if a colon isn't found
+ return fmt.Errorf("no colon found in '%s'", line)
+ }
+
+ // the first substring is the tag
+ reader.currentTag = strings.TrimSpace(substrings[0])
+
+ // determine whether the value contains (or starts) a <text> line
+ substrings = strings.SplitN(substrings[1], "<text>", 2)
+ if len(substrings) == 1 {
+ // no <text> tag found means this is a single-line value
+ // strip whitespace and use as a single line
+ reader.currentValue = strings.TrimSpace(substrings[0])
+ } else {
+ // there was a <text> tag; now decide whether it's multi-line
+ substrings = strings.SplitN(substrings[1], "</text>", 2)
+ if len(substrings) > 1 {
+ // there is also a </text> tag; take the middle part and
+ // set as value
+ reader.currentValue = substrings[0]
+ } else {
+ // there is no </text> tag on this line; switch to midtext
+ reader.currentValue = substrings[0] + "\n"
+ reader.midtext = true
+ return nil
+ }
+ }
+
+ // if we got here, the value was on a single line
+ // so go ahead and add it to the tag-value list
+ tv := tagvalue{reader.currentTag, reader.currentValue}
+ reader.tvList = append(reader.tvList, tv)
+
+ // and reset
+ reader.currentTag = ""
+ reader.currentValue = ""
+
+ return nil
+}
+
+func (reader *tvReader) readNextLineFromMidtext(line string) error {
+ // look for whether the line closes here
+ substrings := strings.SplitN(line, "</text>", 2)
+ if len(substrings) == 1 {
+ // doesn't contain </text>, so keep building the current value
+ reader.currentValue += line + "\n"
+ return nil
+ }
+
+ // contains </text>, so end and record this pair
+ reader.currentValue += substrings[0]
+ tv := tagvalue{reader.currentTag, reader.currentValue}
+ reader.tvList = append(reader.tvList, tv)
+
+ // and reset
+ reader.midtext = false
+ reader.currentTag = ""
+ reader.currentValue = ""
+
+ return nil
+}
diff --git a/tvloader/reader/reader_test.go b/tvloader/reader/reader_test.go
new file mode 100644
index 0000000..73b4c9f
--- /dev/null
+++ b/tvloader/reader/reader_test.go
@@ -0,0 +1,563 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package reader
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestCanReadTagValues(t *testing.T) {
+ sText := `
+Tag1: Value1
+Tag2: Value2
+
+Tag3: <text>line 1
+ line 2</text>
+# Comment
+Tag4: Value4
+Tag5: Value 5
+`
+ sReader := strings.NewReader(sText)
+
+ tvPairList, err := ReadTagValues(sReader)
+ if err != nil {
+ t.Errorf("got error when calling ReadTagValues: %v", err)
+ }
+ if len(tvPairList) != 5 {
+ t.Fatalf("expected len(tvPairList) to be 5, got %d", len(tvPairList))
+ }
+ if tvPairList[0].Tag != "Tag1" {
+ t.Errorf("expected tvPairList[0].Tag to be Tag1, got %s", tvPairList[0].Tag)
+ }
+ if tvPairList[0].Value != "Value1" {
+ t.Errorf("expected tvPairList[0].Value to be Value1, got %s", tvPairList[0].Value)
+ }
+ if tvPairList[1].Tag != "Tag2" {
+ t.Errorf("expected tvPairList[1].Tag to be Tag2, got %s", tvPairList[1].Tag)
+ }
+ if tvPairList[1].Value != "Value2" {
+ t.Errorf("expected tvPairList[1].Value to be Value2, got %s", tvPairList[1].Value)
+ }
+ if tvPairList[2].Tag != "Tag3" {
+ t.Errorf("expected tvPairList[2].Tag to be Tag3, got %s", tvPairList[2].Tag)
+ }
+ if tvPairList[2].Value != "line 1\n line 2" {
+ t.Errorf("expected tvPairList[2].Value to be line 1\n line 2, got %s", tvPairList[2].Value)
+ }
+ if tvPairList[3].Tag != "Tag4" {
+ t.Errorf("expected tvPairList[3].Tag to be Tag4, got %s", tvPairList[3].Tag)
+ }
+ if tvPairList[3].Value != "Value4" {
+ t.Errorf("expected tvPairList[3].Value to be Value4, got %s", tvPairList[3].Value)
+ }
+ if tvPairList[4].Tag != "Tag5" {
+ t.Errorf("expected tvPairList[4].Tag to be Tag5, got %s", tvPairList[4].Tag)
+ }
+ if tvPairList[4].Value != "Value 5" {
+ t.Errorf("expected tvPairList[4].Value to be Value 5, got %s", tvPairList[4].Value)
+ }
+}
+
+func TestCanGetTVListWithFinalize(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag:value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ tvList, err := reader.finalize()
+ if err != nil {
+ t.Errorf("got error when calling finalize: %v", err)
+ }
+ if len(tvList) != 1 || tvList[0].tag != "Tag" || tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", tvList)
+ }
+}
+
+func TestCanGetTVListIncludingMultilineWithFinalize(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag:<text>value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ err = reader.readNextLine("rest of value</text>")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ tvList, err := reader.finalize()
+ if err != nil {
+ t.Errorf("got error when calling finalize: %v", err)
+ }
+ if len(tvList) != 1 || tvList[0].tag != "Tag" || tvList[0].value != "value\nrest of value" {
+ t.Errorf("got invalid tag/value list: %v", tvList)
+ }
+}
+
+func TestCannotFinalizeIfInMidtextState(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag:<text>value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ _, err = reader.finalize()
+ if err == nil {
+ t.Errorf("should have gotten error when calling finalize midtext")
+ }
+}
+
+func TestCurrentLineIncreasesOnEachReadCall(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag:value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+
+ reader.currentLine = 23
+ err = reader.readNextLine("Tag:value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if reader.currentLine != 24 {
+ t.Errorf("expected %d for currentLine, got %d", 23, reader.currentLine)
+ }
+}
+
+func TestReadyCanReadSingleTagValue(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag:value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestReadyCanStripWhitespaceFromValue(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag: value ")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+}
+
+func TestReadyCannotReadLineWithNoColon(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("No colon should be an error")
+ if err == nil {
+ t.Errorf("should have gotten error when calling readNextLine")
+ }
+}
+
+func TestReadyTextTagSwitchesToMidtext(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag: <text>This begins a multiline value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if !reader.midtext {
+ t.Errorf("expected midtext to be true, got false")
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "Tag" {
+ t.Errorf("expected %s for currentTag, got %s", "Tag", reader.currentTag)
+ }
+ if reader.currentValue != "This begins a multiline value\n" {
+ t.Errorf("expected %s for currentValue, got %s", "This begins a multiline value\n", reader.currentValue)
+ }
+}
+
+func TestReadyTextTagAndClosingTagInOneLineFinishesRead(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag: <text>Just one line</text>")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "Just one line" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestCanReadMultilineTextAcrossThreeLines(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag: <text>This value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ err = reader.readNextLine("is three")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ err = reader.readNextLine("lines long</text>")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "This value\nis three\nlines long" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 3 {
+ t.Errorf("expected %d for currentLine, got %d", 3, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestMidtextContinuesIfNoClosingText(t *testing.T) {
+ reader := &tvReader{}
+ reader.midtext = true
+ reader.currentLine = 1
+ reader.currentTag = "Multiline"
+ reader.currentValue = "First line\n"
+
+ err := reader.readNextLine("Second line")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if !reader.midtext {
+ t.Errorf("expected midtext to be true, got false")
+ }
+ if reader.currentLine != 2 {
+ t.Errorf("expected %d for currentLine, got %d", 2, reader.currentLine)
+ }
+ if reader.currentTag != "Multiline" {
+ t.Errorf("expected %s for currentTag, got %s", "Multiline", reader.currentTag)
+ }
+ if reader.currentValue != "First line\nSecond line\n" {
+ t.Errorf("expected %s for currentValue, got %s", "First line\nSecond line\n", reader.currentValue)
+ }
+}
+
+func TestMidtextFinishesIfReachingClosingText(t *testing.T) {
+ reader := &tvReader{}
+ reader.midtext = true
+ reader.currentLine = 1
+ reader.currentTag = "Multiline"
+ reader.currentValue = "First line\n"
+
+ err := reader.readNextLine("Second line</text>")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Multiline" || reader.tvList[0].value != "First line\nSecond line" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 2 {
+ t.Errorf("expected %d for currentLine, got %d", 2, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresCommentLines(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("# this is a comment")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestMidtextIncludesCommentLines(t *testing.T) {
+ reader := &tvReader{}
+ reader.midtext = true
+ reader.currentLine = 1
+ reader.currentTag = "Multiline"
+ reader.currentValue = "First line\n"
+
+ err := reader.readNextLine("# This is part of multiline text")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if !reader.midtext {
+ t.Errorf("expected midtext to be true, got false")
+ }
+ if reader.currentLine != 2 {
+ t.Errorf("expected %d for currentLine, got %d", 2, reader.currentLine)
+ }
+ if reader.currentTag != "Multiline" {
+ t.Errorf("expected %s for currentTag, got %s", "Multiline", reader.currentTag)
+ }
+ if reader.currentValue != "First line\n# This is part of multiline text\n" {
+ t.Errorf("expected %s for currentValue, got %s", "First line\n# This is part of multiline text\n", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresEmptyLines(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestMidtextIncludesEmptyLines(t *testing.T) {
+ reader := &tvReader{}
+ reader.midtext = true
+ reader.currentLine = 1
+ reader.currentTag = "Multiline"
+ reader.currentValue = "First line\n"
+
+ err := reader.readNextLine("")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if !reader.midtext {
+ t.Errorf("expected midtext to be true, got false")
+ }
+ if reader.currentLine != 2 {
+ t.Errorf("expected %d for currentLine, got %d", 2, reader.currentLine)
+ }
+ if reader.currentTag != "Multiline" {
+ t.Errorf("expected %s for currentTag, got %s", "Multiline", reader.currentTag)
+ }
+ if reader.currentValue != "First line\n\n" {
+ t.Errorf("expected %s for currentValue, got %s", "First line\n\n", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresWhitespaceOnlyLines(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine(" \t\t\t ")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestMidtextIncludesWhitespaceOnlyLines(t *testing.T) {
+ reader := &tvReader{}
+ reader.midtext = true
+ reader.currentLine = 1
+ reader.currentTag = "Multiline"
+ reader.currentValue = "First line\n"
+
+ err := reader.readNextLine(" \t\t ")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if !reader.midtext {
+ t.Errorf("expected midtext to be true, got false")
+ }
+ if reader.currentLine != 2 {
+ t.Errorf("expected %d for currentLine, got %d", 2, reader.currentLine)
+ }
+ if reader.currentTag != "Multiline" {
+ t.Errorf("expected %s for currentTag, got %s", "Multiline", reader.currentTag)
+ }
+ if reader.currentValue != "First line\n \t\t \n" {
+ t.Errorf("expected %s for currentValue, got %s", "First line\n \t\t \n", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresSpacesBeforeTag(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine(" \t Tag:value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresSpacesBeforeCommentLines(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine(" \t\t # this is a comment")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+
+ if len(reader.tvList) != 0 {
+ t.Errorf("expected empty tag/value list, got %v", reader.tvList)
+ }
+ if reader.midtext {
+ t.Errorf("expected midtext to be false, got true")
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresSpacesBetweenTagAndColon(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag \t :value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresSpacesBetweenColonAndValue(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag: \t value")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
+
+func TestReadyIgnoresSpacesAfterEndOfValue(t *testing.T) {
+ reader := &tvReader{}
+ err := reader.readNextLine("Tag:value \t ")
+ if err != nil {
+ t.Errorf("got error when calling readNextLine: %v", err)
+ }
+ if len(reader.tvList) != 1 || reader.tvList[0].tag != "Tag" || reader.tvList[0].value != "value" {
+ t.Errorf("got invalid tag/value list: %v", reader.tvList)
+ }
+ if reader.currentLine != 1 {
+ t.Errorf("expected %d for currentLine, got %d", 1, reader.currentLine)
+ }
+ if reader.currentTag != "" {
+ t.Errorf("expected empty string for currentTag, got %s", reader.currentTag)
+ }
+ if reader.currentValue != "" {
+ t.Errorf("expected empty string for currentValue, got %s", reader.currentValue)
+ }
+}
diff --git a/tvloader/tvloader.go b/tvloader/tvloader.go
new file mode 100644
index 0000000..ece2c2b
--- /dev/null
+++ b/tvloader/tvloader.go
@@ -0,0 +1,64 @@
+// Package tvloader is used to load and parse SPDX tag-value documents
+// into tools-golang data structures.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+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"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/tvloader/parser2v1"
+ "github.com/spdx/tools-golang/tvloader/parser2v2"
+ "github.com/spdx/tools-golang/tvloader/reader"
+)
+
+// Load2_1 takes an io.Reader and returns a fully-parsed SPDX Document
+// (version 2.1) if parseable, or error if any error is encountered.
+func Load2_1(content io.Reader) (*v2_1.Document, error) {
+ tvPairs, err := reader.ReadTagValues(content)
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := parser2v1.ParseTagValues(tvPairs)
+ if err != nil {
+ return nil, err
+ }
+
+ return doc, nil
+}
+
+// Load2_2 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_2(content io.Reader) (*v2_2.Document, error) {
+ tvPairs, err := reader.ReadTagValues(content)
+ if err != nil {
+ return nil, err
+ }
+
+ doc, err := parser2v2.ParseTagValues(tvPairs)
+ if err != nil {
+ return nil, err
+ }
+
+ 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/saver2v1/save_annotation.go b/tvsaver/saver2v1/save_annotation.go
new file mode 100644
index 0000000..3fa351f
--- /dev/null
+++ b/tvsaver/saver2v1/save_annotation.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderAnnotation2_1(ann *v2_1.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/saver2v1/save_annotation_test.go b/tvsaver/saver2v1/save_annotation_test.go
new file mode 100644
index 0000000..405bf8a
--- /dev/null
+++ b/tvsaver/saver2v1/save_annotation_test.go
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Annotation section Saver tests =====
+func TestSaver2_1AnnotationSavesTextForPerson(t *testing.T) {
+ ann := &v2_1.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_1(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_1AnnotationSavesTextForOrganization(t *testing.T) {
+ ann := &v2_1.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_1(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_1AnnotationSavesTextForTool(t *testing.T) {
+ ann := &v2_1.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_1(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/saver2v1/save_creation_info.go b/tvsaver/saver2v1/save_creation_info.go
new file mode 100644
index 0000000..e4b5992
--- /dev/null
+++ b/tvsaver/saver2v1/save_creation_info.go
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderCreationInfo2_1(ci *v2_1.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/saver2v1/save_creation_info_test.go b/tvsaver/saver2v1/save_creation_info_test.go
new file mode 100644
index 0000000..5225735
--- /dev/null
+++ b/tvsaver/saver2v1/save_creation_info_test.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Creation Info section Saver tests =====
+func TestSaver2_1CISavesText(t *testing.T) {
+ ci := &v2_1.CreationInfo{
+ LicenseListVersion: "2.0",
+ 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: 2.0
+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_1(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_1CIOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ // --- need at least one creator; do first for Persons ---
+ ci1 := &v2_1.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_1(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_1.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_1(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/saver2v1/save_document.go b/tvsaver/saver2v1/save_document.go
new file mode 100644
index 0000000..1db4bf6
--- /dev/null
+++ b/tvsaver/saver2v1/save_document.go
@@ -0,0 +1,104 @@
+// Package saver2v1 contains functions to render and write a tag-value
+// formatted version of an in-memory SPDX document and its sections
+// (version 2.1).
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// RenderDocument2_1 is the main entry point to take an SPDX in-memory
+// Document (version 2.1), 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_1(doc *v2_1.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_1(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_1(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_1(pkg, w)
+ }
+
+ if len(doc.OtherLicenses) > 0 {
+ fmt.Fprintf(w, "##### Other Licenses\n\n")
+ for _, ol := range doc.OtherLicenses {
+ renderOtherLicense2_1(ol, w)
+ }
+ }
+
+ if len(doc.Relationships) > 0 {
+ fmt.Fprintf(w, "##### Relationships\n\n")
+ for _, rln := range doc.Relationships {
+ renderRelationship2_1(rln, w)
+ }
+ fmt.Fprintf(w, "\n")
+ }
+
+ if len(doc.Annotations) > 0 {
+ fmt.Fprintf(w, "##### Annotations\n\n")
+ for _, ann := range doc.Annotations {
+ renderAnnotation2_1(ann, w)
+ fmt.Fprintf(w, "\n")
+ }
+ }
+
+ if len(doc.Reviews) > 0 {
+ fmt.Fprintf(w, "##### Reviews\n\n")
+ for _, rev := range doc.Reviews {
+ renderReview2_1(rev, w)
+ }
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v1/save_document_test.go b/tvsaver/saver2v1/save_document_test.go
new file mode 100644
index 0000000..1447c07
--- /dev/null
+++ b/tvsaver/saver2v1/save_document_test.go
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== entire Document Saver tests =====
+func TestSaver2_1DocumentSavesText(t *testing.T) {
+
+ // Creation Info section
+ ci := &v2_1.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // unpackaged files
+ f1 := &v2_1.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_1.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_1.File{
+ f1,
+ f2,
+ }
+
+ // Package 1: packaged files with snippets
+ sn1 := &v2_1.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_1.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_1.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_1.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ },
+ }
+
+ f4 := &v2_1.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_1.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_1.File{
+ f3,
+ f4,
+ },
+ }
+
+ // Other Licenses 1 and 2
+ ol1 := &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ }
+
+ ol2 := &v2_1.OtherLicense{
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: `License 2 text - this is a license that does some stuff`,
+ LicenseName: "License 2",
+ }
+
+ // Relationships
+ rln1 := &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln2 := &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1231"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln3 := &v2_1.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1232"),
+ Relationship: "DESCRIBES",
+ }
+
+ // Annotations
+ ann1 := &v2_1.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_1.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_1.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ }
+ rev2 := &v2_1.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_1.Document{
+ SPDXVersion: "SPDX-2.1",
+ DataLicense: "CC0-1.0",
+ SPDXIdentifier: common.ElementID("DOCUMENT"),
+ DocumentName: "spdx-go-0.0.1.abcdef",
+ DocumentNamespace: "https://github.com/swinslow/spdx-docs/spdx-go/spdx-go-0.0.1.abcdef.whatever",
+ CreationInfo: ci,
+ Packages: []*v2_1.Package{
+ pkgWith,
+ },
+ Files: unFiles,
+ OtherLicenses: []*v2_1.OtherLicense{
+ ol1,
+ ol2,
+ },
+ Relationships: []*v2_1.Relationship{
+ rln1,
+ rln2,
+ rln3,
+ },
+ Annotations: []*v2_1.Annotation{
+ ann1,
+ ann2,
+ },
+ Reviews: []*v2_1.Review{
+ rev1,
+ rev2,
+ },
+ }
+
+ want := bytes.NewBufferString(`SPDXVersion: SPDX-2.1
+DataLicense: CC0-1.0
+SPDXID: SPDXRef-DOCUMENT
+DocumentName: spdx-go-0.0.1.abcdef
+DocumentNamespace: https://github.com/swinslow/spdx-docs/spdx-go/spdx-go-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_1(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_1DocumentReturnsErrorIfNilCreationInfo(t *testing.T) {
+ doc := &v2_1.Document{}
+
+ var got bytes.Buffer
+ err := RenderDocument2_1(doc, &got)
+ if err == nil {
+ t.Errorf("Expected error, got nil")
+ }
+}
diff --git a/tvsaver/saver2v1/save_file.go b/tvsaver/saver2v1/save_file.go
new file mode 100644
index 0000000..3cb9027
--- /dev/null
+++ b/tvsaver/saver2v1/save_file.go
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderFile2_1(f *v2_1.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.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_1(s, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v1/save_file_test.go b/tvsaver/saver2v1/save_file_test.go
new file mode 100644
index 0000000..ba1d82b
--- /dev/null
+++ b/tvsaver/saver2v1/save_file_test.go
@@ -0,0 +1,306 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== File section Saver tests =====
+func TestSaver2_1FileSavesText(t *testing.T) {
+ f := &v2_1.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_1.ArtifactOfProject{
+ &v2_1.ArtifactOfProject{
+ Name: "project1",
+ HomePage: "http://example.com/1/",
+ URI: "http://example.com/1/uri.whatever",
+ },
+ &v2_1.ArtifactOfProject{
+ Name: "project2",
+ },
+ &v2_1.ArtifactOfProject{
+ Name: "project3",
+ HomePage: "http://example.com/3/",
+ },
+ &v2_1.ArtifactOfProject{
+ 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",
+ },
+ 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
+FileDependency: f-1.txt
+FileDependency: g.txt
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderFile2_1(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_1FileSavesSnippetsAlso(t *testing.T) {
+ sn1 := &v2_1.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_1.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_1.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ }
+
+ f := &v2_1.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_1(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_1FileOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ f := &v2_1.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_1(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_1FileWrapsCopyrightMultiLine(t *testing.T) {
+ f := &v2_1.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_1(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_1FileWrapsCommentsAndNoticesMultiLine(t *testing.T) {
+ f := &v2_1.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_1(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/saver2v1/save_other_license.go b/tvsaver/saver2v1/save_other_license.go
new file mode 100644
index 0000000..b30aecf
--- /dev/null
+++ b/tvsaver/saver2v1/save_other_license.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderOtherLicense2_1(ol *v2_1.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/saver2v1/save_other_license_test.go b/tvsaver/saver2v1/save_other_license_test.go
new file mode 100644
index 0000000..46ef82c
--- /dev/null
+++ b/tvsaver/saver2v1/save_other_license_test.go
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Other License section Saver tests =====
+func TestSaver2_1OtherLicenseSavesText(t *testing.T) {
+ ol := &v2_1.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_1(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_1OtherLicenseOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ ol := &v2_1.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_1(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/saver2v1/save_package.go b/tvsaver/saver2v1/save_package.go
new file mode 100644
index 0000000..762876f
--- /dev/null
+++ b/tvsaver/saver2v1/save_package.go
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderPackage2_1(pkg *v2_1.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.FilesAnalyzed == true {
+ if pkg.IsFilesAnalyzedTagPresent == true {
+ fmt.Fprintf(w, "FilesAnalyzed: true\n")
+ }
+ } else {
+ fmt.Fprintf(w, "FilesAnalyzed: false\n")
+ }
+ if 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))
+ }
+ }
+
+ 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_1(fi, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v1/save_package_test.go b/tvsaver/saver2v1/save_package_test.go
new file mode 100644
index 0000000..4939b3f
--- /dev/null
+++ b/tvsaver/saver2v1/save_package_test.go
@@ -0,0 +1,498 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Package section Saver tests =====
+func TestSaver2_1PackageSavesTextCombo1(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_1.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_1.PackageExternalReference{
+ Category: "PACKAGE-MANAGER",
+ RefType: "npm",
+ Locator: "p1@0.1.0",
+ ExternalRefComment: `this is a
+multi-line external ref comment`,
+ }
+
+ per3 := &v2_1.PackageExternalReference{
+ Category: "OTHER",
+ RefType: "anything",
+ Locator: "anything-without-spaces-can-go-here",
+ // no ExternalRefComment for this one
+ }
+
+ pkg := &v2_1.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",
+ PackageExternalReferences: []*v2_1.PackageExternalReference{
+ per1,
+ per2,
+ per3,
+ },
+ }
+
+ // 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
+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: OTHER anything anything-without-spaces-can-go-here
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_1(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_1PackageSavesTextCombo2(t *testing.T) {
+ // no package external refs
+ // test Supplier:NOASSERTION, Originator:Organization
+ // FilesAnalyzed true, IsFilesAnalyzedTagPresent false
+ // PackageVerificationCodeExcludedFile is empty
+
+ pkg := &v2_1.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",
+ }
+
+ // 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
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_1(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_1PackageSavesTextCombo3(t *testing.T) {
+ // no package external refs
+ // test Supplier:Person, Originator:NOASSERTION
+ // FilesAnalyzed false, IsFilesAnalyzedTagPresent true
+ // PackageVerificationCodeExcludedFile is empty
+
+ pkg := &v2_1.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",
+ }
+
+ // 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
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderPackage2_1(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_1PackageSaveOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ pkg := &v2_1.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_1(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_1PackageSavesFilesIfPresent(t *testing.T) {
+ f1 := &v2_1.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_1.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_1.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_1.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_1(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_1PackageWrapsCopyrightMultiLine(t *testing.T) {
+ pkg := &v2_1.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_1(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/saver2v1/save_relationship.go b/tvsaver/saver2v1/save_relationship.go
new file mode 100644
index 0000000..01aa3dd
--- /dev/null
+++ b/tvsaver/saver2v1/save_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderRelationship2_1(rln *v2_1.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/saver2v1/save_relationship_test.go b/tvsaver/saver2v1/save_relationship_test.go
new file mode 100644
index 0000000..886670b
--- /dev/null
+++ b/tvsaver/saver2v1/save_relationship_test.go
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Relationship section Saver tests =====
+func TestSaver2_1RelationshipSavesText(t *testing.T) {
+ rln := &v2_1.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_1(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_1RelationshipOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rln := &v2_1.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_1(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_1RelationshipWrapsCommentMultiLine(t *testing.T) {
+ rln := &v2_1.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_1(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/saver2v1/save_review.go b/tvsaver/saver2v1/save_review.go
new file mode 100644
index 0000000..33511a3
--- /dev/null
+++ b/tvsaver/saver2v1/save_review.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderReview2_1(rev *v2_1.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/saver2v1/save_review_test.go b/tvsaver/saver2v1/save_review_test.go
new file mode 100644
index 0000000..10d030b
--- /dev/null
+++ b/tvsaver/saver2v1/save_review_test.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Review section Saver tests =====
+func TestSaver2_1ReviewSavesText(t *testing.T) {
+ rev := &v2_1.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_1(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_1ReviewOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rev := &v2_1.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_1(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_1ReviewWrapsMultiLine(t *testing.T) {
+ rev := &v2_1.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_1(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/saver2v1/save_snippet.go b/tvsaver/saver2v1/save_snippet.go
new file mode 100644
index 0000000..19c9323
--- /dev/null
+++ b/tvsaver/saver2v1/save_snippet.go
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+func renderSnippet2_1(sn *v2_1.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)
+ }
+
+ fmt.Fprintf(w, "\n")
+
+ return nil
+}
diff --git a/tvsaver/saver2v1/save_snippet_test.go b/tvsaver/saver2v1/save_snippet_test.go
new file mode 100644
index 0000000..3c9036a
--- /dev/null
+++ b/tvsaver/saver2v1/save_snippet_test.go
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+)
+
+// ===== Snippet section Saver tests =====
+func TestSaver2_1SnippetSavesText(t *testing.T) {
+ sn := &v2_1.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",
+ }
+
+ // 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
+
+`)
+
+ // render as buffer of bytes
+ var got bytes.Buffer
+ err := renderSnippet2_1(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_1SnippetOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ sn := &v2_1.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_1(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_1SnippetWrapsCopyrightMultiline(t *testing.T) {
+ sn := &v2_1.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_1(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/saver2v1/util.go b/tvsaver/saver2v1/util.go
new file mode 100644
index 0000000..e206449
--- /dev/null
+++ b/tvsaver/saver2v1/util.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+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/saver2v1/util_test.go b/tvsaver/saver2v1/util_test.go
new file mode 100644
index 0000000..aceccb7
--- /dev/null
+++ b/tvsaver/saver2v1/util_test.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v1
+
+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/saver2v2/save_annotation.go b/tvsaver/saver2v2/save_annotation.go
new file mode 100644
index 0000000..f5ea893
--- /dev/null
+++ b/tvsaver/saver2v2/save_annotation.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderAnnotation2_2(ann *v2_2.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/saver2v2/save_annotation_test.go b/tvsaver/saver2v2/save_annotation_test.go
new file mode 100644
index 0000000..e8d1741
--- /dev/null
+++ b/tvsaver/saver2v2/save_annotation_test.go
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Annotation section Saver tests =====
+func TestSaver2_2AnnotationSavesTextForPerson(t *testing.T) {
+ ann := &v2_2.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_2(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_2AnnotationSavesTextForOrganization(t *testing.T) {
+ ann := &v2_2.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_2(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_2AnnotationSavesTextForTool(t *testing.T) {
+ ann := &v2_2.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_2(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/saver2v2/save_creation_info.go b/tvsaver/saver2v2/save_creation_info.go
new file mode 100644
index 0000000..87533a2
--- /dev/null
+++ b/tvsaver/saver2v2/save_creation_info.go
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderCreationInfo2_2(ci *v2_2.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/saver2v2/save_creation_info_test.go b/tvsaver/saver2v2/save_creation_info_test.go
new file mode 100644
index 0000000..bb10aa3
--- /dev/null
+++ b/tvsaver/saver2v2/save_creation_info_test.go
@@ -0,0 +1,112 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Creation Info section Saver tests =====
+func TestSaver2_2CISavesText(t *testing.T) {
+ ci := &v2_2.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_2(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_2CIOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ // --- need at least one creator; do first for Persons ---
+ ci1 := &v2_2.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_2(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_2.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_2(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/saver2v2/save_document.go b/tvsaver/saver2v2/save_document.go
new file mode 100644
index 0000000..efec2a1
--- /dev/null
+++ b/tvsaver/saver2v2/save_document.go
@@ -0,0 +1,104 @@
+// Package saver2v2 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 saver2v2
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// RenderDocument2_2 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_2(doc *v2_2.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_2(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_2(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_2(pkg, w)
+ }
+
+ if len(doc.OtherLicenses) > 0 {
+ fmt.Fprintf(w, "##### Other Licenses\n\n")
+ for _, ol := range doc.OtherLicenses {
+ renderOtherLicense2_2(ol, w)
+ }
+ }
+
+ if len(doc.Relationships) > 0 {
+ fmt.Fprintf(w, "##### Relationships\n\n")
+ for _, rln := range doc.Relationships {
+ renderRelationship2_2(rln, w)
+ }
+ fmt.Fprintf(w, "\n")
+ }
+
+ if len(doc.Annotations) > 0 {
+ fmt.Fprintf(w, "##### Annotations\n\n")
+ for _, ann := range doc.Annotations {
+ renderAnnotation2_2(ann, w)
+ fmt.Fprintf(w, "\n")
+ }
+ }
+
+ if len(doc.Reviews) > 0 {
+ fmt.Fprintf(w, "##### Reviews\n\n")
+ for _, rev := range doc.Reviews {
+ renderReview2_2(rev, w)
+ }
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v2/save_document_test.go b/tvsaver/saver2v2/save_document_test.go
new file mode 100644
index 0000000..39146dd
--- /dev/null
+++ b/tvsaver/saver2v2/save_document_test.go
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== entire Document Saver tests =====
+func TestSaver2_2DocumentSavesText(t *testing.T) {
+
+ // Creation Info section
+ ci := &v2_2.CreationInfo{
+ Creators: []common.Creator{
+ {Creator: "John Doe", CreatorType: "Person"},
+ },
+ Created: "2018-10-10T06:20:00Z",
+ }
+
+ // unpackaged files
+ f1 := &v2_2.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_2.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_2.File{
+ f1,
+ f2,
+ }
+
+ // Package 1: packaged files with snippets
+ sn1 := &v2_2.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_2.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_2.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_2.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ },
+ }
+
+ f4 := &v2_2.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_2.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_2.File{
+ f3,
+ f4,
+ },
+ }
+
+ // Other Licenses 1 and 2
+ ol1 := &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: `License 1 text
+blah blah blah
+blah blah blah blah`,
+ LicenseName: "License 1",
+ }
+
+ ol2 := &v2_2.OtherLicense{
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: `License 2 text - this is a license that does some stuff`,
+ LicenseName: "License 2",
+ }
+
+ // Relationships
+ rln1 := &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "p1"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln2 := &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1231"),
+ Relationship: "DESCRIBES",
+ }
+
+ rln3 := &v2_2.Relationship{
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File1232"),
+ Relationship: "DESCRIBES",
+ }
+
+ // Annotations
+ ann1 := &v2_2.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_2.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_2.Review{
+ Reviewer: "John Doe",
+ ReviewerType: "Person",
+ ReviewDate: "2018-10-14T10:28:00Z",
+ }
+ rev2 := &v2_2.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_2.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_2.Package{
+ pkgWith,
+ },
+ Files: unFiles,
+ OtherLicenses: []*v2_2.OtherLicense{
+ ol1,
+ ol2,
+ },
+ Relationships: []*v2_2.Relationship{
+ rln1,
+ rln2,
+ rln3,
+ },
+ Annotations: []*v2_2.Annotation{
+ ann1,
+ ann2,
+ },
+ Reviews: []*v2_2.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_2(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_2DocumentReturnsErrorIfNilCreationInfo(t *testing.T) {
+ doc := &v2_2.Document{}
+
+ var got bytes.Buffer
+ err := RenderDocument2_2(doc, &got)
+ if err == nil {
+ t.Errorf("Expected error, got nil")
+ }
+}
diff --git a/tvsaver/saver2v2/save_file.go b/tvsaver/saver2v2/save_file.go
new file mode 100644
index 0000000..1c367a8
--- /dev/null
+++ b/tvsaver/saver2v2/save_file.go
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+ "sort"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderFile2_2(f *v2_2.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_2(s, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v2/save_file_test.go b/tvsaver/saver2v2/save_file_test.go
new file mode 100644
index 0000000..f8a8f32
--- /dev/null
+++ b/tvsaver/saver2v2/save_file_test.go
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== File section Saver tests =====
+func TestSaver2_2FileSavesText(t *testing.T) {
+ f := &v2_2.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_2.ArtifactOfProject{
+ &v2_2.ArtifactOfProject{
+ Name: "project1",
+ HomePage: "http://example.com/1/",
+ URI: "http://example.com/1/uri.whatever",
+ },
+ &v2_2.ArtifactOfProject{
+ Name: "project2",
+ },
+ &v2_2.ArtifactOfProject{
+ Name: "project3",
+ HomePage: "http://example.com/3/",
+ },
+ &v2_2.ArtifactOfProject{
+ 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_2(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_2FileSavesSnippetsAlso(t *testing.T) {
+ sn1 := &v2_2.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_2.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_2.Snippet{
+ common.ElementID("Snippet19"): sn1,
+ common.ElementID("Snippet20"): sn2,
+ }
+
+ f := &v2_2.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_2(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_2FileOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ f := &v2_2.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_2(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_2FileWrapsCopyrightMultiLine(t *testing.T) {
+ f := &v2_2.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_2(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_2FileWrapsCommentsAndNoticesMultiLine(t *testing.T) {
+ f := &v2_2.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_2(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/saver2v2/save_other_license.go b/tvsaver/saver2v2/save_other_license.go
new file mode 100644
index 0000000..9fad209
--- /dev/null
+++ b/tvsaver/saver2v2/save_other_license.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderOtherLicense2_2(ol *v2_2.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/saver2v2/save_other_license_test.go b/tvsaver/saver2v2/save_other_license_test.go
new file mode 100644
index 0000000..6b71522
--- /dev/null
+++ b/tvsaver/saver2v2/save_other_license_test.go
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Other License section Saver tests =====
+func TestSaver2_2OtherLicenseSavesText(t *testing.T) {
+ ol := &v2_2.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_2(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_2OtherLicenseOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ ol := &v2_2.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_2(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/saver2v2/save_package.go b/tvsaver/saver2v2/save_package.go
new file mode 100644
index 0000000..387fe2a
--- /dev/null
+++ b/tvsaver/saver2v2/save_package.go
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderPackage2_2(pkg *v2_2.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.FilesAnalyzed == true {
+ if pkg.IsFilesAnalyzedTagPresent == true {
+ fmt.Fprintf(w, "FilesAnalyzed: true\n")
+ }
+ } else {
+ fmt.Fprintf(w, "FilesAnalyzed: false\n")
+ }
+ if 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_2(fi, w)
+ }
+
+ return nil
+}
diff --git a/tvsaver/saver2v2/save_package_test.go b/tvsaver/saver2v2/save_package_test.go
new file mode 100644
index 0000000..45f9851
--- /dev/null
+++ b/tvsaver/saver2v2/save_package_test.go
@@ -0,0 +1,523 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Package section Saver tests =====
+func TestSaver2_2PackageSavesTextCombo1(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_2.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_2.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_2.PackageExternalReference{
+ Category: "PERSISTENT-ID",
+ RefType: "swh",
+ Locator: "swh:1:cnt:94a9ed024d3859793618152ea559a168bbcbb5e2",
+ // no ExternalRefComment for this one
+ }
+
+ per4 := &v2_2.PackageExternalReference{
+ Category: "OTHER",
+ RefType: "anything",
+ Locator: "anything-without-spaces-can-go-here",
+ // no ExternalRefComment for this one
+ }
+
+ pkg := &v2_2.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_2.PackageExternalReference{
+ per1,
+ per2,
+ per3,
+ per4,
+ },
+ }
+
+ // 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
+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_2(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_2PackageSavesTextCombo2(t *testing.T) {
+ // no package external refs
+ // test Supplier:NOASSERTION, Originator:Organization
+ // FilesAnalyzed true, IsFilesAnalyzedTagPresent false
+ // PackageVerificationCodeExcludedFile is empty
+
+ pkg := &v2_2.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_2(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_2PackageSavesTextCombo3(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_2.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_2(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_2PackageSaveOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ pkg := &v2_2.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_2(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_2PackageSavesFilesIfPresent(t *testing.T) {
+ f1 := &v2_2.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_2.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_2.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_2.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_2(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_2PackageWrapsMultiLine(t *testing.T) {
+ pkg := &v2_2.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_2(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/saver2v2/save_relationship.go b/tvsaver/saver2v2/save_relationship.go
new file mode 100644
index 0000000..a962120
--- /dev/null
+++ b/tvsaver/saver2v2/save_relationship.go
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderRelationship2_2(rln *v2_2.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/saver2v2/save_relationship_test.go b/tvsaver/saver2v2/save_relationship_test.go
new file mode 100644
index 0000000..927164c
--- /dev/null
+++ b/tvsaver/saver2v2/save_relationship_test.go
@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Relationship section Saver tests =====
+func TestSaver2_2RelationshipSavesText(t *testing.T) {
+ rln := &v2_2.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_2(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_2RelationshipOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rln := &v2_2.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_2(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_2RelationshipCanHaveNONEOnRight(t *testing.T) {
+ rln := &v2_2.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_2(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_2RelationshipCanHaveNOASSERTIONOnRight(t *testing.T) {
+ rln := &v2_2.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_2(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_2RelationshipWrapsCommentMultiLine(t *testing.T) {
+ rln := &v2_2.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_2(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/saver2v2/save_review.go b/tvsaver/saver2v2/save_review.go
new file mode 100644
index 0000000..8f1add6
--- /dev/null
+++ b/tvsaver/saver2v2/save_review.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderReview2_2(rev *v2_2.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/saver2v2/save_review_test.go b/tvsaver/saver2v2/save_review_test.go
new file mode 100644
index 0000000..961daa1
--- /dev/null
+++ b/tvsaver/saver2v2/save_review_test.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Review section Saver tests =====
+func TestSaver2_2ReviewSavesText(t *testing.T) {
+ rev := &v2_2.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_2(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_2ReviewOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ rev := &v2_2.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_2(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_2ReviewWrapsMultiLine(t *testing.T) {
+ rev := &v2_2.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_2(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/saver2v2/save_snippet.go b/tvsaver/saver2v2/save_snippet.go
new file mode 100644
index 0000000..d1254db
--- /dev/null
+++ b/tvsaver/saver2v2/save_snippet.go
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "fmt"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func renderSnippet2_2(sn *v2_2.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/saver2v2/save_snippet_test.go b/tvsaver/saver2v2/save_snippet_test.go
new file mode 100644
index 0000000..72daf15
--- /dev/null
+++ b/tvsaver/saver2v2/save_snippet_test.go
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+// ===== Snippet section Saver tests =====
+func TestSaver2_2SnippetSavesText(t *testing.T) {
+ sn := &v2_2.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_2(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_2SnippetOmitsOptionalFieldsIfEmpty(t *testing.T) {
+ sn := &v2_2.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_2(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_2SnippetWrapsCopyrightMultiline(t *testing.T) {
+ sn := &v2_2.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_2(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/saver2v2/util.go b/tvsaver/saver2v2/util.go
new file mode 100644
index 0000000..988d2be
--- /dev/null
+++ b/tvsaver/saver2v2/util.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+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/saver2v2/util_test.go b/tvsaver/saver2v2/util_test.go
new file mode 100644
index 0000000..cdbf6f5
--- /dev/null
+++ b/tvsaver/saver2v2/util_test.go
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package saver2v2
+
+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/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
new file mode 100644
index 0000000..a73b4b9
--- /dev/null
+++ b/tvsaver/tvsaver.go
@@ -0,0 +1,36 @@
+// Package tvsaver is used to save tools-golang data structures
+// as SPDX tag-value documents.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package tvsaver
+
+import (
+ "io"
+
+ "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),
+// and writes it to the writer in tag-value format. It returns error
+// if any error is encountered.
+func Save2_1(doc *v2_1.Document, w io.Writer) error {
+ return saver2v1.RenderDocument2_1(doc, w)
+}
+
+// Save2_2 takes an io.Writer and an SPDX Document (version 2.2),
+// and writes it to the writer in tag-value format. It returns error
+// if any error is encountered.
+func Save2_2(doc *v2_2.Document, w io.Writer) error {
+ return saver2v2.RenderDocument2_2(doc, w)
+}
+
+// Save2_3 takes an io.Writer and an SPDX Document (version 2.3),
+// and writes it to the writer in tag-value format. It returns error
+// if any error is encountered.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ return saver2v3.RenderDocument2_3(doc, w)
+}
diff --git a/utils/filesystem.go b/utils/filesystem.go
new file mode 100644
index 0000000..bb8c53e
--- /dev/null
+++ b/utils/filesystem.go
@@ -0,0 +1,124 @@
+// Package utils contains various utility functions to support the
+// main tools-golang packages.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package utils
+
+import (
+ "crypto/md5"
+ "crypto/sha1"
+ "crypto/sha256"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// GetAllFilePaths takes a path to a directory (including an optional slice of
+// path patterns to ignore), and returns a slice of relative paths to all files
+// in that directory and its subdirectories (excluding those that are ignored).
+func GetAllFilePaths(dirRoot string, pathsIgnored []string) ([]string, error) {
+ // paths is a _pointer_ to a slice -- not just a slice.
+ // this is so that it can be appropriately modified by append
+ // in the sub-function.
+ paths := &[]string{}
+ prefix := strings.TrimSuffix(dirRoot, "/")
+
+ err := filepath.Walk(dirRoot, func(path string, fi os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ // don't include path if it's a directory
+ if fi.IsDir() {
+ return nil
+ }
+ // don't include path if it's a symbolic link
+ if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
+ return nil
+ }
+
+ shortPath := strings.TrimPrefix(path, prefix)
+
+ // don't include path if it should be ignored
+ if pathsIgnored != nil && ShouldIgnore(shortPath, pathsIgnored) {
+ return nil
+ }
+
+ // if we got here, record the path
+ *paths = append(*paths, shortPath)
+ return nil
+ })
+
+ return *paths, err
+}
+
+// GetHashesForFilePath takes a path to a file on disk, and returns
+// SHA1, SHA256 and MD5 hashes for that file as strings.
+func GetHashesForFilePath(p string) (string, string, string, error) {
+ f, err := os.Open(p)
+ if err != nil {
+ return "", "", "", err
+ }
+ defer f.Close()
+
+ var ssha1, ssha256, smd5 string
+ hSHA1 := sha1.New()
+ hSHA256 := sha256.New()
+ hMD5 := md5.New()
+ hMulti := io.MultiWriter(hSHA1, hSHA256, hMD5)
+
+ if _, err := io.Copy(hMulti, f); err != nil {
+ f.Close()
+ return "", "", "", err
+ }
+ ssha1 = fmt.Sprintf("%x", hSHA1.Sum(nil))
+ ssha256 = fmt.Sprintf("%x", hSHA256.Sum(nil))
+ smd5 = fmt.Sprintf("%x", hMD5.Sum(nil))
+
+ return ssha1, ssha256, smd5, nil
+}
+
+// ShouldIgnore compares a file path to a slice of file path patterns,
+// and determines whether that file should be ignored because it matches
+// any of those patterns.
+func ShouldIgnore(fileName string, pathsIgnored []string) bool {
+ fDirs, fFile := filepath.Split(fileName)
+
+ for _, pattern := range pathsIgnored {
+ // split into dir(s) and filename
+ patternDirs, patternFile := filepath.Split(pattern)
+ patternDirStars := strings.HasPrefix(patternDirs, "**")
+ if patternDirStars {
+ patternDirs = patternDirs[2:]
+ }
+
+ // case 1: specific file
+ if !patternDirStars && patternDirs == fDirs && patternFile != "" && patternFile == fFile {
+ return true
+ }
+
+ // case 2: all files in specific directory
+ if !patternDirStars && strings.HasPrefix(fDirs, patternDirs) && patternFile == "" {
+ return true
+ }
+
+ // case 3: specific file in any dir
+ if patternDirStars && patternDirs == "/" && patternFile != "" && patternFile == fFile {
+ return true
+ }
+
+ // case 4: specific file in any matching subdir
+ if patternDirStars && strings.Contains(fDirs, patternDirs) && patternFile != "" && patternFile == fFile {
+ return true
+ }
+
+ // case 5: any file in any matching subdir
+ if patternDirStars && strings.Contains(fDirs, patternDirs) && patternFile == "" {
+ return true
+ }
+
+ }
+
+ // if no match, don't ignore
+ return false
+}
diff --git a/utils/filesystem_test.go b/utils/filesystem_test.go
new file mode 100644
index 0000000..70e1291
--- /dev/null
+++ b/utils/filesystem_test.go
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package utils
+
+import (
+ "testing"
+)
+
+// ===== Filesystem and hash functionality tests =====
+func TestFilesystemCanGetSliceOfFolderContents(t *testing.T) {
+ dirRoot := "../testdata/project1/"
+
+ filePaths, err := GetAllFilePaths(dirRoot, nil)
+ if err != nil {
+ t.Fatalf("expected filePaths, got error: %v", err)
+ }
+ if filePaths == nil {
+ t.Fatalf("expected non-nil filePaths, got nil")
+ }
+ // should only be 5 files
+ // symbolic link in project1/symbolic-link should be ignored
+ if len(filePaths) != 5 {
+ t.Fatalf("expected %v, got %v", 5, len(filePaths))
+ }
+
+ // should be in alphabetical order, with files prefixed with '/'
+ if filePaths[0] != "/emptyfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/emptyfile.testdata.txt", filePaths[0])
+ }
+ if filePaths[1] != "/file1.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/file1.testdata.txt", filePaths[1])
+ }
+ if filePaths[2] != "/file3.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/file3.testdata.txt", filePaths[2])
+ }
+ if filePaths[3] != "/folder1/file4.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/folder1/file4.testdata.txt", filePaths[3])
+ }
+ if filePaths[4] != "/lastfile.testdata.txt" {
+ t.Errorf("expected %v, got %v", "/lastfile.testdata.txt", filePaths[4])
+ }
+}
+
+func TestFilesystemGetAllFilePathsFailsForNonExistentDirectory(t *testing.T) {
+ dirRoot := "./does/not/exist/"
+
+ _, err := GetAllFilePaths(dirRoot, nil)
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+func TestFilesystemCanIgnoreFilesWhenGettingFilePaths(t *testing.T) {
+ dirRoot := "../testdata/project3/"
+ pathsIgnored := []string{
+ "**/ignoredir/",
+ "/excludedir/",
+ "**/ignorefile.txt",
+ "/alsoEXCLUDEthis.txt",
+ }
+
+ filePaths, err := GetAllFilePaths(dirRoot, pathsIgnored)
+ if err != nil {
+ t.Fatalf("expected filePaths, got error: %v", err)
+ }
+ if filePaths == nil {
+ t.Fatalf("expected non-nil filePaths, got nil")
+ }
+
+ // should only be 5 files
+ if len(filePaths) != 5 {
+ t.Fatalf("expected %v, got %v", 5, len(filePaths))
+ }
+
+ // should be in alphabetical order, with files prefixed with '/'
+ if filePaths[0] != "/dontscan.txt" {
+ t.Errorf("expected %v, got %v", "/dontscan.txt", filePaths[0])
+ }
+ if filePaths[1] != "/keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "/keep/keep.txt", filePaths[1])
+ }
+ if filePaths[2] != "/keep.txt" {
+ t.Errorf("expected %v, got %v", "/keep.txt", filePaths[2])
+ }
+ if filePaths[3] != "/subdir/keep/dontscan.txt" {
+ t.Errorf("expected %v, got %v", "/subdir/keep/dontscan.txt", filePaths[3])
+ }
+ if filePaths[4] != "/subdir/keep/keep.txt" {
+ t.Errorf("expected %v, got %v", "/subdir/keep/keep.txt", filePaths[4])
+ }
+
+}
+
+// FIXME add test to make sure we get an error for a directory without
+// FIXME appropriate permissions to read its (sub)contents
+
+func TestFilesystemGetsHashesForFilePath(t *testing.T) {
+ f := "../testdata/project1/file1.testdata.txt"
+
+ ssha1, ssha256, smd5, err := GetHashesForFilePath(f)
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if ssha1 != "024f870eb6323f532515f7a09d5646a97083b819" {
+ t.Errorf("expected %v, got %v", "024f870eb6323f532515f7a09d5646a97083b819", ssha1)
+ }
+ if ssha256 != "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf" {
+ t.Errorf("expected %v, got %v", "b14e44284ca477b4c0db34b15ca4c454b2947cce7883e22321cf2984050e15bf", ssha256)
+ }
+ if smd5 != "37c8208479dfe42d2bb29debd6e32d4a" {
+ t.Errorf("expected %v, got %v", "37c8208479dfe42d2bb29debd6e32d4a", smd5)
+ }
+}
+
+func TestFilesystemGetsErrorWhenRequestingHashesForInvalidFilePath(t *testing.T) {
+ f := "./does/not/exist"
+
+ _, _, _, err := GetHashesForFilePath(f)
+ if err == nil {
+ t.Errorf("expected non-nil error, got nil")
+ }
+}
+
+// FIXME add test to make sure we get an error for hashes for a file without
+// FIXME appropriate permissions to read its contents
+
+func TestFilesystemExcludesForIgnoredPaths(t *testing.T) {
+ // one specific file
+ pathsIgnored := []string{"/file.txt"}
+ fileName := "/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/fileNope.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+
+ // two specific files
+ pathsIgnored = []string{"/file.txt", "/file2.txt"}
+ fileName = "/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/fileNope.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/file2.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+
+ // one specific file in specific subdirectory
+ pathsIgnored = []string{"/subdir/file.txt"}
+ fileName = "/subdir/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/file.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/something/subdir/file.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+
+ // one specific file in any directory
+ pathsIgnored = []string{"**/file.txt"}
+ fileName = "/subdir/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/something/subdir/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/something/fileNope.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+
+ // all files in one specific subdirectory (and its subdirectories)
+ pathsIgnored = []string{"/subdir/"}
+ fileName = "/subdir/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/file.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/subdir/sub2/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/nope/subdir/file.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+
+ // all files in subdirectory with this name, wherever it appears
+ pathsIgnored = []string{"**/subdir/"}
+ fileName = "/subdir/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/file.txt"
+ if ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/subdir/sub2/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+ fileName = "/nope/subdir/file.txt"
+ if !ShouldIgnore(fileName, pathsIgnored) {
+ t.Errorf("incorrect for %v, ignoring %v", fileName, pathsIgnored)
+ }
+
+}
diff --git a/utils/verification.go b/utils/verification.go
new file mode 100644
index 0000000..72523b3
--- /dev/null
+++ b/utils/verification.go
@@ -0,0 +1,133 @@
+// Package utils contains various utility functions to support the
+// main tools-golang packages.
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+package utils
+
+import (
+ "crypto/sha1"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// GetVerificationCode2_1 takes a slice of files and an optional filename
+// for an "excludes" file, and returns a Package Verification Code calculated
+// according to SPDX spec version 2.1, section 3.9.4.
+func GetVerificationCode2_1(files []*v2_1.File, excludeFile string) (common.PackageVerificationCode, error) {
+ // create slice of strings - unsorted SHA1s for all files
+ shas := []string{}
+ for i, f := range files {
+ if f == nil {
+ return common.PackageVerificationCode{}, fmt.Errorf("got nil file for identifier %v", i)
+ }
+ if f.FileName != excludeFile {
+ // find the SHA1 hash, if present
+ for _, checksum := range f.Checksums {
+ if checksum.Algorithm == common.SHA1 {
+ shas = append(shas, checksum.Value)
+ }
+ }
+ }
+ }
+
+ // sort the strings
+ sort.Strings(shas)
+
+ // concatenate them into one string, with no trailing separators
+ shasConcat := strings.Join(shas, "")
+
+ // and get its SHA1 value
+ hsha1 := sha1.New()
+ hsha1.Write([]byte(shasConcat))
+ bs := hsha1.Sum(nil)
+
+ code := common.PackageVerificationCode{
+ Value: fmt.Sprintf("%x", bs),
+ ExcludedFiles: []string{excludeFile},
+ }
+
+ return code, nil
+}
+
+// GetVerificationCode2_2 takes a slice of files and an optional filename
+// for an "excludes" file, and returns a Package Verification Code calculated
+// according to SPDX spec version 2.2, section 3.9.4.
+func GetVerificationCode2_2(files []*v2_2.File, excludeFile string) (common.PackageVerificationCode, error) {
+ // create slice of strings - unsorted SHA1s for all files
+ shas := []string{}
+ for i, f := range files {
+ if f == nil {
+ return common.PackageVerificationCode{}, fmt.Errorf("got nil file for identifier %v", i)
+ }
+ if f.FileName != excludeFile {
+ // find the SHA1 hash, if present
+ for _, checksum := range f.Checksums {
+ if checksum.Algorithm == common.SHA1 {
+ shas = append(shas, checksum.Value)
+ }
+ }
+ }
+ }
+
+ // sort the strings
+ sort.Strings(shas)
+
+ // concatenate them into one string, with no trailing separators
+ shasConcat := strings.Join(shas, "")
+
+ // and get its SHA1 value
+ hsha1 := sha1.New()
+ hsha1.Write([]byte(shasConcat))
+ bs := hsha1.Sum(nil)
+
+ code := common.PackageVerificationCode{
+ Value: fmt.Sprintf("%x", bs),
+ ExcludedFiles: []string{excludeFile},
+ }
+
+ return code, nil
+}
+
+// GetVerificationCode2_3 takes a slice of files and an optional filename
+// for an "excludes" file, and returns a Package Verification Code calculated
+// according to SPDX spec version 2.3, section 3.9.4.
+func GetVerificationCode2_3(files []*v2_3.File, excludeFile string) (common.PackageVerificationCode, error) {
+ // create slice of strings - unsorted SHA1s for all files
+ shas := []string{}
+ for i, f := range files {
+ if f == nil {
+ return common.PackageVerificationCode{}, fmt.Errorf("got nil file for identifier %v", i)
+ }
+ if f.FileName != excludeFile {
+ // find the SHA1 hash, if present
+ for _, checksum := range f.Checksums {
+ if checksum.Algorithm == common.SHA1 {
+ shas = append(shas, checksum.Value)
+ }
+ }
+ }
+ }
+
+ // sort the strings
+ sort.Strings(shas)
+
+ // concatenate them into one string, with no trailing separators
+ shasConcat := strings.Join(shas, "")
+
+ // and get its SHA1 value
+ hsha1 := sha1.New()
+ hsha1.Write([]byte(shasConcat))
+ bs := hsha1.Sum(nil)
+
+ code := common.PackageVerificationCode{
+ Value: fmt.Sprintf("%x", bs),
+ ExcludedFiles: []string{excludeFile},
+ }
+
+ return code, nil
+}
diff --git a/utils/verification_test.go b/utils/verification_test.go
new file mode 100644
index 0000000..beee8b6
--- /dev/null
+++ b/utils/verification_test.go
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package utils
+
+import (
+ "testing"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_1"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+// ===== 2.1 Verification code functionality tests =====
+
+func TestPackage2_1CanGetVerificationCode(t *testing.T) {
+ files := []*v2_1.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{{Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{{Value: "3333333333bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{{Value: "8888888888bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file5.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{{Value: "2222222222bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{{Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa", Algorithm: common.SHA1}},
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "ac924b375119c81c1f08c3e2722044bfbbdcd3dc"}
+
+ gotCode, err := GetVerificationCode2_1(files, "")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+
+}
+
+func TestPackage2_1CanGetVerificationCodeIgnoringExcludesFile(t *testing.T) {
+ files := []*v2_1.File{
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{{Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{{Value: "3333333333bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "thisfile.spdx",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{{Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{{Value: "8888888888bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{{Value: "2222222222bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "17fab1bd18fe5c13b5d3983f1c17e5f88b8ff266"}
+
+ gotCode, err := GetVerificationCode2_1(files, "thisfile.spdx")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+}
+
+func TestPackage2_1GetVerificationCodeFailsIfNilFileInSlice(t *testing.T) {
+ files := []*v2_1.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{{Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ nil,
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{{Value: "8888888888bbbbbbbbbbccccccccccdddddddddd", Algorithm: common.SHA1}},
+ },
+ }
+
+ _, err := GetVerificationCode2_1(files, "")
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.2 Verification code functionality tests =====
+
+func TestPackage2_2CanGetVerificationCode(t *testing.T) {
+ files := []*v2_2.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "3333333333bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file5.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "2222222222bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa",
+ },
+ },
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "ac924b375119c81c1f08c3e2722044bfbbdcd3dc"}
+
+ gotCode, err := GetVerificationCode2_2(files, "")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+
+}
+
+func TestPackage2_2CanGetVerificationCodeIgnoringExcludesFile(t *testing.T) {
+ files := []*v2_2.File{
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "3333333333bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "thisfile.spdx",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa",
+ },
+ },
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "2222222222bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "17fab1bd18fe5c13b5d3983f1c17e5f88b8ff266"}
+
+ gotCode, err := GetVerificationCode2_2(files, "thisfile.spdx")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+}
+
+func TestPackage2_2GetVerificationCodeFailsIfNilFileInSlice(t *testing.T) {
+ files := []*v2_2.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ nil,
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ }
+
+ _, err := GetVerificationCode2_2(files, "")
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
+
+// ===== 2.3 Verification code functionality tests =====
+
+func TestPackage2_3CanGetVerificationCode(t *testing.T) {
+ files := []*v2_3.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "3333333333bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file5.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "2222222222bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa",
+ },
+ },
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "ac924b375119c81c1f08c3e2722044bfbbdcd3dc"}
+
+ gotCode, err := GetVerificationCode2_3(files, "")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+
+}
+
+func TestPackage2_3CanGetVerificationCodeIgnoringExcludesFile(t *testing.T) {
+ files := []*v2_3.File{
+ {
+ FileName: "file1.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File1",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "3333333333bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "thisfile.spdx",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "bbbbbbbbbbccccccccccddddddddddaaaaaaaaaa",
+ },
+ },
+ },
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File3",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ {
+ FileName: "file4.txt",
+ FileSPDXIdentifier: "File4",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "2222222222bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ }
+
+ wantCode := common.PackageVerificationCode{Value: "17fab1bd18fe5c13b5d3983f1c17e5f88b8ff266"}
+
+ gotCode, err := GetVerificationCode2_3(files, "thisfile.spdx")
+ if err != nil {
+ t.Fatalf("expected nil error, got %v", err)
+ }
+ if wantCode.Value != gotCode.Value {
+ t.Errorf("expected %v, got %v", wantCode, gotCode)
+ }
+}
+
+func TestPackage2_3GetVerificationCodeFailsIfNilFileInSlice(t *testing.T) {
+ files := []*v2_3.File{
+ {
+ FileName: "file2.txt",
+ FileSPDXIdentifier: "File0",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ nil,
+ {
+ FileName: "file3.txt",
+ FileSPDXIdentifier: "File2",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: common.SHA1,
+ Value: "8888888888bbbbbbbbbbccccccccccdddddddddd",
+ },
+ },
+ },
+ }
+
+ _, err := GetVerificationCode2_3(files, "")
+ if err == nil {
+ t.Fatalf("expected non-nil error, got nil")
+ }
+}
diff --git a/yaml/parser.go b/yaml/parser.go
new file mode 100644
index 0000000..265a04d
--- /dev/null
+++ b/yaml/parser.go
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_yaml
+
+import (
+ "bytes"
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "sigs.k8s.io/yaml"
+)
+
+// Load2_2 takes in an io.Reader and returns an SPDX document.
+func Load2_2(content io.Reader) (*v2_2.Document, error) {
+ // convert io.Reader to a slice of bytes and call the parser
+ buf := new(bytes.Buffer)
+ _, err := buf.ReadFrom(content)
+ if err != nil {
+ return nil, err
+ }
+
+ var doc v2_2.Document
+ err = yaml.Unmarshal(buf.Bytes(), &doc)
+ if err != nil {
+ return nil, err
+ }
+
+ return &doc, nil
+}
+
+// Load2_3 takes in an io.Reader and returns an SPDX document.
+func Load2_3(content io.Reader) (*v2_3.Document, error) {
+ // convert io.Reader to a slice of bytes and call the parser
+ buf := new(bytes.Buffer)
+ _, err := buf.ReadFrom(content)
+ if err != nil {
+ return nil, err
+ }
+
+ var doc v2_3.Document
+ err = yaml.Unmarshal(buf.Bytes(), &doc)
+ if err != nil {
+ return nil, err
+ }
+
+ return &doc, nil
+}
diff --git a/yaml/writer.go b/yaml/writer.go
new file mode 100644
index 0000000..06ef69a
--- /dev/null
+++ b/yaml/writer.go
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_yaml
+
+import (
+ "io"
+
+ "github.com/spdx/tools-golang/spdx/v2_2"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+ "sigs.k8s.io/yaml"
+)
+
+// Save2_2 takes an SPDX Document (version 2.2) and an io.Writer, and writes the document to the writer in YAML format.
+func Save2_2(doc *v2_2.Document, w io.Writer) error {
+ buf, err := yaml.Marshal(doc)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Save2_3 takes an SPDX Document (version 2.3) and an io.Writer, and writes the document to the writer in YAML format.
+func Save2_3(doc *v2_3.Document, w io.Writer) error {
+ buf, err := yaml.Marshal(doc)
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/yaml/yaml_v2_2_test.go b/yaml/yaml_v2_2_test.go
new file mode 100644
index 0000000..9d66f5b
--- /dev/null
+++ b/yaml/yaml_v2_2_test.go
@@ -0,0 +1,451 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_yaml
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_2"
+)
+
+func TestLoad2_2(t *testing.T) {
+ file, err := os.Open("../examples/sample-docs/yaml/SPDXYAMLExample-2.2.spdx.yaml")
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := Load2_2(file)
+ if err != nil {
+ t.Errorf("yaml.parser.Load2_2() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_2
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("got incorrect struct after parsing YAML example")
+ return
+ }
+}
+
+func TestWrite2_2(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_2
+ if err := Save2_2(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_2() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original handwritten struct
+ parsedDoc, err := Load2_2(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("got incorrect struct after writing and re-parsing YAML example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example YAML SPDX v2.2 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want2_2 = v2_2.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.2",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_2.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_2.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_2.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_2.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_2.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_2.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_2.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_2.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",
+ },
+ },
+ Files: []*v2_2.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_2.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_2.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_2.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",
+ },
+ },
+}
diff --git a/yaml/yaml_v2_3_test.go b/yaml/yaml_v2_3_test.go
new file mode 100644
index 0000000..0271150
--- /dev/null
+++ b/yaml/yaml_v2_3_test.go
@@ -0,0 +1,492 @@
+// SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+
+package spdx_yaml
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "github.com/spdx/tools-golang/spdx/common"
+ "github.com/spdx/tools-golang/spdx/v2_3"
+)
+
+var update = *flag.Bool("update-snapshots", false, "update the example snapshot")
+
+func TestLoad2_3(t *testing.T) {
+ fileName := "../examples/sample-docs/yaml/SPDXYAMLExample-2.3.spdx.yaml"
+
+ if update {
+ f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ t.Errorf("unable to write SPDX 2.3 example to YAML: %v", err)
+ }
+ err = Save2_3(&want2_3, f)
+ if err != nil {
+ t.Errorf("unable to serialize SPDX 2.3 example to YAML: %v", err)
+ }
+ }
+
+ file, err := os.Open(fileName)
+ if err != nil {
+ panic(fmt.Errorf("error opening File: %s", err))
+ }
+
+ got, err := Load2_3(file)
+ if err != nil {
+ t.Errorf("yaml.parser.Load2_3() error = %v", err)
+ return
+ }
+
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+
+ if cmp.Equal(handwrittenExample, got) {
+ t.Errorf("got incorrect struct after parsing YAML example")
+ return
+ }
+}
+
+func TestWrite2_3(t *testing.T) {
+ w := &bytes.Buffer{}
+ // get a copy of the handwritten struct so we don't mutate it on accident
+ handwrittenExample := want2_3
+ if err := Save2_3(&handwrittenExample, w); err != nil {
+ t.Errorf("Save2_3() error = %v", err.Error())
+ return
+ }
+
+ // we should be able to parse what the writer wrote, and it should be identical to the original handwritten struct
+ parsedDoc, err := Load2_3(bytes.NewReader(w.Bytes()))
+ if err != nil {
+ t.Errorf("failed to parse written document: %v", err.Error())
+ return
+ }
+
+ if cmp.Equal(handwrittenExample, parsedDoc) {
+ t.Errorf("got incorrect struct after writing and re-parsing YAML example")
+ return
+ }
+}
+
+// want is handwritten translation of the official example YAML SPDX v2.2 document into a Go struct.
+// We expect that the result of parsing the official document should be this value.
+// We expect that the result of writing this struct should match the official example document.
+var want2_3 = v2_3.Document{
+ DataLicense: "CC0-1.0",
+ SPDXVersion: "SPDX-2.2",
+ SPDXIdentifier: "SPDXRef-DOCUMENT",
+ DocumentName: "SPDX-Tools-v2.0",
+ DocumentNamespace: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301",
+ CreationInfo: &v2_3.CreationInfo{
+ LicenseListVersion: "3.9",
+ Creators: []common.Creator{
+ {CreatorType: "Tool", Creator: "LicenseFind-1.0"},
+ {CreatorType: "Organization", Creator: "ExampleCodeInspect ()"},
+ {CreatorType: "Person", Creator: "Jane Doe ()"},
+ },
+ Created: "2010-01-29T18:30:22Z",
+ CreatorComment: "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.",
+ },
+ DocumentComment: "This document was created using SPDX 2.0 using licenses from the web site.",
+ ExternalDocumentReferences: []v2_3.ExternalDocumentRef{
+ {
+ DocumentRefID: "DocumentRef-spdx-tool-1.2",
+ URI: "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301",
+ Checksum: common.Checksum{
+ Algorithm: common.SHA1,
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2759",
+ },
+ },
+ },
+ OtherLicenses: []*v2_3.OtherLicense{
+ {
+ LicenseIdentifier: "LicenseRef-1",
+ ExtractedText: "/*\n * (c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-2",
+ ExtractedText: "This package includes the GRDDL parser developed by Hewlett Packard under the following license:\n� Copyright 2007 Hewlett-Packard Development Company, LP\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: \n\nRedistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. \nRedistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. \nThe name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. \nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-4",
+ ExtractedText: "/*\n * (c) Copyright 2009 University of Bristol\n * All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * 2. Redistributions in binary form must reproduce the above copyright\n * notice, this list of conditions and the following disclaimer in the\n * documentation and/or other materials provided with the distribution.\n * 3. The name of the author may not be used to endorse or promote products\n * derived from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/",
+ },
+ {
+ LicenseIdentifier: "LicenseRef-Beerware-4.2",
+ ExtractedText: "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+ LicenseComment: "The beerware license has a couple of other standard variants.",
+ LicenseName: "Beer-Ware License (Version 42)",
+ LicenseCrossReferences: []string{"http://people.freebsd.org/~phk/"},
+ },
+ {
+ LicenseIdentifier: "LicenseRef-3",
+ ExtractedText: "The CyberNeko Software License, Version 1.0\n\n \n(C) Copyright 2002-2005, Andy Clark. All rights reserved.\n \nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer. \n\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n3. The end-user documentation included with the redistribution,\n if any, must include the following acknowledgment: \n \"This product includes software developed by Andy Clark.\"\n Alternately, this acknowledgment may appear in the software itself,\n if and wherever such third-party acknowledgments normally appear.\n\n4. The names \"CyberNeko\" and \"NekoHTML\" must not be used to endorse\n or promote products derived from this software without prior \n written permission. For written permission, please contact \n andyc@cyberneko.net.\n\n5. Products derived from this software may not be called \"CyberNeko\",\n nor may \"CyberNeko\" appear in their name, without prior written\n permission of the author.\n\nTHIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED\nWARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR OTHER CONTRIBUTORS\nBE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, \nOR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT \nOF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR \nBUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, \nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE \nOR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, \nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.",
+ LicenseName: "CyberNeko License",
+ LicenseCrossReferences: []string{
+ "http://people.apache.org/~andyc/neko/LICENSE",
+ "http://justasample.url.com",
+ },
+ LicenseComment: "This is tye CyperNeko License",
+ },
+ },
+ Annotations: []*v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Jane Doe ()",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Document level annotation",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Joe Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2010-02-10T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses",
+ },
+ {
+ Annotator: common.Annotator{
+ Annotator: "Suzanne Reviewer",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-03-13T00:00:00Z",
+ AnnotationType: "REVIEW",
+ AnnotationComment: "Another example reviewer.",
+ },
+ },
+ Packages: []*v2_3.Package{
+ {
+ PackageName: "glibc",
+ PackageSPDXIdentifier: "SPDXRef-Package",
+ PackageVersion: "2.11.1",
+ PackageFileName: "glibc-2.11.1.tar.gz",
+ PackageSupplier: &common.Supplier{
+ Supplier: "Jane Doe (jane.doe@example.com)",
+ SupplierType: "Person",
+ },
+ PackageOriginator: &common.Originator{
+ Originator: "ExampleCodeInspect (contact@example.com)",
+ OriginatorType: "Organization",
+ },
+ PackageDownloadLocation: "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+ FilesAnalyzed: true,
+ PackageVerificationCode: &common.PackageVerificationCode{
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ ExcludedFiles: []string{"./package.spdx"},
+ },
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ {
+ Algorithm: "SHA256",
+ Value: "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd",
+ },
+ },
+ PackageHomePage: "http://ftp.gnu.org/gnu/glibc",
+ PackageSourceInfo: "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
+ PackageLicenseConcluded: "(LGPL-2.0-only OR LicenseRef-3)",
+ PackageLicenseInfoFromFiles: []string{
+ "GPL-2.0-only",
+ "LicenseRef-2",
+ "LicenseRef-1",
+ },
+ PackageLicenseDeclared: "(LGPL-2.0-only AND LicenseRef-3)",
+ PackageLicenseComments: "The license for this project changed with the release of version x.y. The version of the project included here post-dates the license change.",
+ PackageCopyrightText: "Copyright 2008-2010 John Smith",
+ PackageSummary: "GNU C library.",
+ PackageDescription: "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.",
+ PackageComment: "",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "SECURITY",
+ RefType: "cpe23Type",
+ Locator: "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+ },
+ {
+ Category: "OTHER",
+ RefType: "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+ Locator: "acmecorp/acmenator/4.1.3-alpha",
+ ExternalRefComment: "This is the external ref for Acme",
+ },
+ },
+ PackageAttributionTexts: []string{
+ "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.",
+ },
+ Files: nil,
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "Package Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "Package level annotation",
+ },
+ },
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-1",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://commons.apache.org/proper/commons-lang/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageName: "Apache Commons Lang",
+ },
+ {
+ PackageName: "Jena",
+ PackageSPDXIdentifier: "SPDXRef-fromDoap-0",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDownloadLocation: "https://search.maven.org/remotecontent?filepath=org/apache/jena/apache-jena/3.12.0/apache-jena-3.12.0.tar.gz",
+ PackageExternalReferences: []*v2_3.PackageExternalReference{
+ {
+ Category: "PACKAGE_MANAGER",
+ RefType: "purl",
+ Locator: "pkg:maven/org.apache.jena/apache-jena@3.12.0",
+ },
+ },
+ FilesAnalyzed: false,
+ PackageHomePage: "http://www.openjena.org/",
+ PackageLicenseConcluded: "NOASSERTION",
+ PackageLicenseDeclared: "NOASSERTION",
+ PackageVersion: "3.12.0",
+ },
+ {
+ PackageSPDXIdentifier: "SPDXRef-Saxon",
+ PackageChecksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "85ed0817af83a24ad8da68c2b5094de69833983c",
+ },
+ },
+ PackageCopyrightText: "Copyright Saxonica Ltd",
+ PackageDescription: "The Saxon package is a collection of tools for processing XML documents.",
+ PackageDownloadLocation: "https://sourceforge.net/projects/saxon/files/Saxon-B/8.8.0.7/saxonb8-8-0-7j.zip/download",
+ FilesAnalyzed: false,
+ PackageHomePage: "http://saxon.sourceforge.net/",
+ PackageLicenseComments: "Other versions available for a commercial license",
+ PackageLicenseConcluded: "MPL-1.0",
+ PackageLicenseDeclared: "MPL-1.0",
+ PackageName: "Saxon",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "8.8",
+ },
+ {
+ PrimaryPackagePurpose: "CONTAINER",
+ PackageSPDXIdentifier: "SPDXRef-CentOS-7",
+ PackageCopyrightText: "NOASSERTION",
+ PackageDescription: "The CentOS container used to run the application.",
+ PackageDownloadLocation: "NOASSERTION",
+ FilesAnalyzed: false,
+ PackageHomePage: "https://www.centos.org/",
+ PackageName: "centos",
+ PackageFileName: "saxonB-8.8.zip",
+ PackageVersion: "centos7.9.2009",
+ BuiltDate: "2021-09-15T02:38:00Z",
+ ValidUntilDate: "2022-10-15T02:38:00Z",
+ ReleaseDate: "2021-10-15T02:38:00Z",
+ },
+ },
+ Files: []*v2_3.File{
+ {
+ FileName: "./src/org/spdx/parser/DOAPProject.java",
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ FileTypes: []string{
+ "SOURCE",
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
+ },
+ },
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{
+ "Apache-2.0",
+ },
+ FileCopyrightText: "Copyright 2010, 2011 Source Auditor Inc.",
+ FileContributors: []string{
+ "Protecode Inc.",
+ "SPDX Technical Team Members",
+ "Open Logic Inc.",
+ "Source Auditor Inc.",
+ "Black Duck Software In.c",
+ },
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-CommonsLangSrc",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "c2b4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file is used by Jena",
+ FileCopyrightText: "Copyright 2001-2011 The Apache Software Foundation",
+ FileContributors: []string{"Apache Software Foundation"},
+ FileName: "./lib-source/commons-lang3-3.1-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseConcluded: "Apache-2.0",
+ LicenseInfoInFiles: []string{"Apache-2.0"},
+ FileNotice: "Apache Commons Lang\nCopyright 2001-2011 The Apache Software Foundation\n\nThis product includes software developed by\nThe Apache Software Foundation (http://www.apache.org/).\n\nThis product includes software from the Spring Framework,\nunder the Apache License 2.0 (see: StringUtils.containsWhitespace())",
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-JenaLib",
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "3ab4e1c67a2d28fced849ee1bb76e7391b93f125",
+ },
+ },
+ FileComment: "This file belongs to Jena",
+ FileCopyrightText: "(c) Copyright 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009 Hewlett-Packard Development Company, LP",
+ FileContributors: []string{"Apache Software Foundation", "Hewlett Packard Inc."},
+ FileName: "./lib-source/jena-2.6.3-sources.jar",
+ FileTypes: []string{"ARCHIVE"},
+ LicenseComments: "This license is used by Jena",
+ LicenseConcluded: "LicenseRef-1",
+ LicenseInfoInFiles: []string{"LicenseRef-1"},
+ },
+ {
+ FileSPDXIdentifier: "SPDXRef-File",
+ Annotations: []v2_3.Annotation{
+ {
+ Annotator: common.Annotator{
+ Annotator: "File Commenter",
+ AnnotatorType: "Person",
+ },
+ AnnotationDate: "2011-01-29T18:30:22Z",
+ AnnotationType: "OTHER",
+ AnnotationComment: "File level annotation",
+ },
+ },
+ Checksums: []common.Checksum{
+ {
+ Algorithm: "SHA1",
+ Value: "d6a770ba38583ed4bb4525bd96e50461655d2758",
+ },
+ {
+ Algorithm: "MD5",
+ Value: "624c1abb3664f4b35547e7c73864ad24",
+ },
+ },
+ FileComment: "The concluded license was taken from the package level that the file was included in.\nThis information was found in the COPYING.txt file in the xyz directory.",
+ FileCopyrightText: "Copyright 2008-2010 John Smith",
+ FileContributors: []string{"The Regents of the University of California", "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"},
+ FileName: "./package/foo.c",
+ FileTypes: []string{"SOURCE"},
+ LicenseComments: "The concluded license was taken from the package level that the file was included in.",
+ LicenseConcluded: "(LGPL-2.0-only OR LicenseRef-2)",
+ LicenseInfoInFiles: []string{"GPL-2.0-only", "LicenseRef-2"},
+ FileNotice: "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the �Software�), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED �AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
+ },
+ },
+ Snippets: []v2_3.Snippet{
+ {
+ SnippetSPDXIdentifier: "SPDXRef-Snippet",
+ SnippetFromFileSPDXIdentifier: "SPDXRef-DoapSource",
+ Ranges: []common.SnippetRange{
+ {
+ StartPointer: common.SnippetRangePointer{
+ Offset: 310,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ Offset: 420,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ {
+ StartPointer: common.SnippetRangePointer{
+ LineNumber: 5,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ EndPointer: common.SnippetRangePointer{
+ LineNumber: 23,
+ FileSPDXIdentifier: "SPDXRef-DoapSource",
+ },
+ },
+ },
+ SnippetLicenseConcluded: "GPL-2.0-only",
+ LicenseInfoInSnippet: []string{"GPL-2.0-only"},
+ SnippetLicenseComments: "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.",
+ SnippetCopyrightText: "Copyright 2008-2010 John Smith",
+ SnippetComment: "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.",
+ SnippetName: "from linux kernel",
+ SnippetAttributionTexts: []string{"Snippet attribution"},
+ },
+ },
+ Relationships: []*v2_3.Relationship{
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ RelationshipComment: "A relationship comment",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("spdx-tool-1.2", "ToolsElement"),
+ Relationship: "COPY_OF",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "File"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "DOCUMENT"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "DESCRIBES",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "JenaLib"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "Package"),
+ RefB: common.MakeDocElementID("", "Saxon"),
+ Relationship: "DYNAMIC_LINK",
+ },
+ {
+ RefA: common.MakeDocElementID("", "CommonsLangSrc"),
+ RefB: common.MakeDocElementSpecial("NOASSERTION"),
+ Relationship: "GENERATED_FROM",
+ },
+ {
+ RefA: common.MakeDocElementID("", "JenaLib"),
+ RefB: common.MakeDocElementID("", "Package"),
+ Relationship: "CONTAINS",
+ },
+ {
+ RefA: common.MakeDocElementID("", "File"),
+ RefB: common.MakeDocElementID("", "fromDoap-0"),
+ Relationship: "GENERATED_FROM",
+ },
+ },
+ Reviews: []*v2_3.Review{
+ {
+ Reviewer: "joe@example.com",
+ ReviewerType: "Person",
+ ReviewDate: "2021-11-03T05:43:21Z",
+ ReviewComment: "This is a review comment",
+ },
+ },
+}