diff --git a/data/SPDXRdfExample.rdf b/data/SPDXRdfExample.rdf index 4ada369ba..66d71323f 100644 --- a/data/SPDXRdfExample.rdf +++ b/data/SPDXRdfExample.rdf @@ -3,7 +3,7 @@ xmlns:j.0="http://usefulinc.com/ns/doap#" xmlns="http://spdx.org/rdf/terms#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"> - + 2010-02-03T00:00:00Z diff --git a/data/SPDXSimpleTag.tag b/data/SPDXSimpleTag.tag index 8e7e16ff7..e2f979580 100644 --- a/data/SPDXSimpleTag.tag +++ b/data/SPDXSimpleTag.tag @@ -1,6 +1,7 @@ # Document info SPDXVersion: SPDX-2.1 DataLicense: CC0-1.0 +DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 DocumentComment: Sample Comment # Creation info diff --git a/data/SPDXTagExample.tag b/data/SPDXTagExample.tag index 25261122b..f0e341216 100644 --- a/data/SPDXTagExample.tag +++ b/data/SPDXTagExample.tag @@ -1,5 +1,6 @@ SPDXVersion: SPDX-2.1 DataLicense: CC0-1.0 +DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 DocumentComment: This is a sample spreadsheet ## Creation Information diff --git a/spdx/document.py b/spdx/document.py index 7d307fc84..bbb7ecfd9 100644 --- a/spdx/document.py +++ b/spdx/document.py @@ -190,6 +190,7 @@ class Document(object): - version: Spec version. Mandatory, one - Type: Version. - data_license: SPDX-Metadata license. Mandatory, one. Type: License. - comment: Comments on the SPDX file, optional one. Type: str + - namespace: SPDX document specific namespace. Mandatory, one. Type: str - creation_info: SPDX file creation info. Mandatory, one. Type: CreationInfo - package: Package described by this document. Mandatory, one. Type: Package - extracted_licenses: List of licenses extracted that are not part of the @@ -198,12 +199,14 @@ class Document(object): Type: Review. """ - def __init__(self, version=None, data_license=None, comment=None, package=None): + def __init__(self, version=None, data_license=None, comment=None, + namespace=None, package=None): # avoid recursive impor from spdx.creationinfo import CreationInfo self.version = version self.data_license = data_license self.comment = comment + self.namespace = namespace self.creation_info = CreationInfo() self.package = package self.extracted_licenses = [] @@ -237,6 +240,7 @@ def validate(self, messages=None): return (self.validate_version(messages) and self.validate_data_lics(messages) + and self.validate_namespace(messages) and self.validate_creation_info(messages) and self.validate_package(messages) and self.validate_extracted_licenses(messages) @@ -268,6 +272,16 @@ def validate_data_lics(self, messages=None): messages.append('Document data license must be CC0-1.0.') return False + def validate_namespace(self, messages=None): + # FIXME: messages should be returned + messages = messages if messages is not None else [] + + if self.namespace is None: + messages.append('Document has no namespace.') + return False + else: + return True + def validate_reviews(self, messages=None): # FIXME: messages should be returned messages = messages if messages is not None else [] diff --git a/spdx/parsers/lexers/tagvalue.py b/spdx/parsers/lexers/tagvalue.py index 53f1573c3..dbbd71c6e 100644 --- a/spdx/parsers/lexers/tagvalue.py +++ b/spdx/parsers/lexers/tagvalue.py @@ -22,6 +22,7 @@ class Lexer(object): 'SPDXVersion': 'DOC_VERSION', 'DataLicense': 'DOC_LICENSE', 'DocumentComment': 'DOC_COMMENT', + 'DocumentNamespace': 'DOC_NAMESPACE', # Creation info 'Creator': 'CREATOR', 'Created': 'CREATED', diff --git a/spdx/parsers/rdf.py b/spdx/parsers/rdf.py index 7da300990..c392e33a8 100644 --- a/spdx/parsers/rdf.py +++ b/spdx/parsers/rdf.py @@ -33,6 +33,8 @@ ERROR_MESSAGES = { 'DOC_VERS_VALUE': 'Invalid specVersion \'{0}\' must be SPDX-M.N where M and N are numbers.', 'DOC_D_LICS': 'Invalid dataLicense \'{0}\' must be http://spdx.org/licenses/CC0-1.0.', + 'DOC_NAMESPACE_VALUE': 'Invalid DocumentNamespace value {0}, must contain a scheme (e.g. "https:") ' + 'and should not contain the "#" delimiter.', 'LL_VALUE': 'Invalid licenseListVersion \'{0}\' must be of the format N.N where N is a number', 'CREATED_VALUE': 'Invalid created value \'{0}\' must be date in ISO 8601 format.', 'CREATOR_VALUE': 'Invalid creator value \'{0}\' must be Organization, Tool or Person.', @@ -799,7 +801,15 @@ def parse_creation_info(self, ci_term): self.value_error('LL_VALUE', o) def parse_doc_fields(self, doc_term): - """Parses the version, data license and comment.""" + """Parses the version, data license, comment, and namespace.""" + try: + if doc_term.count('#', 0, len(doc_term)) <= 1: + doc_namespace = doc_term.split('#')[0] + self.builder.set_doc_namespace(self.doc, doc_namespace) + else: + self.value_error('DOC_NAMESPACE_VALUE', doc_term) + except SPDXValueError: + self.value_error('DOC_NAMESPACE_VALUE', doc_term) for _s, _p, o in self.graph.triples((doc_term, self.spdx_namespace['specVersion'], None)): try: self.builder.set_doc_version(self.doc, six.text_type(o)) diff --git a/spdx/parsers/rdfbuilders.py b/spdx/parsers/rdfbuilders.py index a18ef6f15..d7706c1e6 100644 --- a/spdx/parsers/rdfbuilders.py +++ b/spdx/parsers/rdfbuilders.py @@ -23,6 +23,7 @@ from spdx.parsers.builderexceptions import OrderError from spdx.parsers.builderexceptions import SPDXValueError from spdx.parsers import tagvaluebuilders +from spdx.parsers import validations class DocBuilder(object): @@ -80,6 +81,21 @@ def set_doc_comment(self, doc, comment): else: raise CardinalityError('Document::Comment') + def set_doc_namespace(self, doc, namespace): + """Sets the document namespace. + Raise SPDXValueError if malformed value, CardinalityError + if already defined. + """ + if not self.doc_namespace_set: + self.doc_namespace_set = True + if validations.validate_doc_namespace(namespace): + doc.namespace = namespace + return True + else: + raise SPDXValueError('Document::Namespace') + else: + raise CardinalityError('Document::Comment') + def reset_document(self): """ Reset the internal state to allow building new document @@ -87,6 +103,7 @@ def reset_document(self): # FIXME: this state does not make sense self.doc_version_set = False self.doc_comment_set = False + self.doc_namespace_set = False self.doc_data_lics_set = False diff --git a/spdx/parsers/tagvalue.py b/spdx/parsers/tagvalue.py index dee664436..1c0d75f45 100644 --- a/spdx/parsers/tagvalue.py +++ b/spdx/parsers/tagvalue.py @@ -40,6 +40,10 @@ 'DOC_VERSION_VALUE': 'Invalid SPDXVersion \'{0}\' must be SPDX-M.N where M and N are numbers. Line: {1}', 'DOC_VERSION_VALUE_TYPE': 'Invalid SPDXVersion value, must be SPDX-M.N where M and N are numbers. Line: {0}', 'DOC_COMMENT_VALUE_TYPE': 'DocumentComment value must be free form text between tags, line:{0}', + 'DOC_NAMESPACE_VALUE': 'Invalid DocumentNamespace value {0}, must contain a scheme (e.g. "https:") ' + 'and should not contain the "#" delimiter, line:{1}', + 'DOC_NAMESPACE_VALUE_TYPE': 'Invalid DocumentNamespace value, must contain a scheme (e.g. "https:") ' + 'and should not contain the "#" delimiter, line: {0}', 'REVIEWER_VALUE_TYPE': 'Invalid Reviewer value must be a Person, Organization or Tool. Line: {0}', 'CREATOR_VALUE_TYPE': 'Invalid Reviewer value must be a Person, Organization or Tool. Line: {0}', 'REVIEW_DATE_VALUE_TYPE': 'ReviewDate value must be date in ISO 8601 format, line: {0}', @@ -107,6 +111,7 @@ def p_attrib(self, p): """attrib : spdx_version | data_lics | doc_comment + | doc_namespace | creator | created | creator_comment @@ -1077,6 +1082,27 @@ def p_doc_comment_2(self, p): msg = ERROR_MESSAGES['DOC_COMMENT_VALUE_TYPE'].format(p.lineno(1)) self.logger.log(msg) + def p_doc_namespace_1(self, p): + """doc_namespace : DOC_NAMESPACE LINE""" + try: + if six.PY2: + value = p[2].decode(encoding='utf-8') + else: + value = p[2] + self.builder.set_doc_namespace(self.document, value) + except SPDXValueError: + self.error = True + msg = ERROR_MESSAGES['DOC_NAMESPACE_VALUE'].format(p[2], p.lineno(2)) + self.logger.log(msg) + except CardinalityError: + self.more_than_one_error('DocumentNamespace', p.lineno(1)) + + def p_doc_namespace_2(self, p): + """doc_namespace : DOC_NAMESPACE error""" + self.error = True + msg = ERROR_MESSAGES['DOC_NAMESPACE_VALUE_TYPE'].format(p.lineno(1)) + self.logger.log(msg) + def p_data_license_1(self, p): """data_lics : DOC_LICENSE LINE""" try: diff --git a/spdx/parsers/tagvaluebuilders.py b/spdx/parsers/tagvaluebuilders.py index 385c51c4e..da1c7f7f2 100644 --- a/spdx/parsers/tagvaluebuilders.py +++ b/spdx/parsers/tagvaluebuilders.py @@ -117,11 +117,27 @@ def set_doc_comment(self, doc, comment): else: raise CardinalityError('Document::Comment') + def set_doc_namespace(self, doc, namespace): + """Sets the document namespace. + Raise SPDXValueError if malformed value, CardinalityError + if already defined. + """ + if not self.doc_namespace_set: + self.doc_namespace_set = True + if validations.validate_doc_namespace(namespace): + doc.namespace = namespace + return True + else: + raise SPDXValueError('Document::Namespace') + else: + raise CardinalityError('Document::Comment') + def reset_document(self): """Resets the state to allow building new documents""" # FIXME: this state does not make sense self.doc_version_set = False self.doc_comment_set = False + self.doc_namespace_set = False self.doc_data_lics_set = False diff --git a/spdx/parsers/validations.py b/spdx/parsers/validations.py index 11ac460ea..6641d5f8f 100644 --- a/spdx/parsers/validations.py +++ b/spdx/parsers/validations.py @@ -100,6 +100,16 @@ def validate_doc_comment(value, optional=False): return validate_is_free_form_text(value, optional) +def validate_doc_namespace(value, optional=False): + if value is None: + return optional + elif ((value.startswith('http://') or value.startswith('https://') or + value.startswith('ftp://')) and ('#' not in value)): + return True + else: + return False + + def validate_creator(value, optional=False): if value is None: return optional diff --git a/spdx/writers/tagvalue.py b/spdx/writers/tagvalue.py index 9af92ef1e..66abd9e3b 100644 --- a/spdx/writers/tagvalue.py +++ b/spdx/writers/tagvalue.py @@ -236,6 +236,7 @@ def write_document(document, out, validate=True): out.write('# Document Information\n\n') write_value('SPDXVersion', str(document.version), out) write_value('DataLicense', document.data_license.identifier, out) + write_value('DocumentNamespace', document.namespace, out) if document.has_comment: write_text_value('DocumentComment', document.comment, out) write_separators(out) diff --git a/tests/data/doc_write/tv-simple-plus.tv b/tests/data/doc_write/tv-simple-plus.tv index 77baa7d89..b9ae6154d 100644 --- a/tests/data/doc_write/tv-simple-plus.tv +++ b/tests/data/doc_write/tv-simple-plus.tv @@ -1,6 +1,7 @@ # Document Information SPDXVersion: SPDX-2.1 DataLicense: CC0-1.0 +DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 # Creation Info # Package PackageName: some/path diff --git a/tests/data/doc_write/tv-simple.tv b/tests/data/doc_write/tv-simple.tv index a41928039..e3a301cb8 100644 --- a/tests/data/doc_write/tv-simple.tv +++ b/tests/data/doc_write/tv-simple.tv @@ -1,6 +1,7 @@ # Document Information SPDXVersion: SPDX-2.1 DataLicense: CC0-1.0 +DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 # Creation Info # Package PackageName: some/path diff --git a/tests/test_builder.py b/tests/test_builder.py index 93a32dd54..e76eebe89 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -63,6 +63,22 @@ def test_data_lics_cardinality(self): self.builder.set_doc_data_lics(self.document, lics_str) self.builder.set_doc_data_lics(self.document, lics_str) + def test_correct_doc_namespace(self): + doc_namespace_str = 'https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301' + self.builder.set_doc_namespace(self.document, doc_namespace_str) + assert self.document.namespace == doc_namespace_str + + @testing_utils.raises(builders.SPDXValueError) + def test_doc_namespace_value(self): + doc_namespace_str = 'https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#SPDXRef-DOCUMENT' + self.builder.set_doc_data_lics(self.document, doc_namespace_str) + + @testing_utils.raises(builders.CardinalityError) + def test_doc_namespace_cardinality(self): + doc_namespace_str = 'https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301' + self.builder.set_doc_namespace(self.document, doc_namespace_str) + self.builder.set_doc_namespace(self.document, doc_namespace_str) + def test_correct_data_comment(self): comment_str = 'This is a comment.' comment_text = '' + comment_str + '' diff --git a/tests/test_document.py b/tests/test_document.py index 69c7dd4b3..b9e1ef7f6 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -66,7 +66,8 @@ def test_creation(self): assert document.data_license.identifier == 'AFL-1.1' def test_document_validate_failures_returns_informative_messages(self): - doc = Document(Version(2, 1), License.from_identifier('CC0-1.0')) + doc = Document(Version(2, 1), License.from_identifier('CC0-1.0'), + namespace='https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301') pack = doc.package = Package('some/path', NoAssert()) file1 = File('./some/path/tofile') file1.name = './some/path/tofile' @@ -83,7 +84,8 @@ def test_document_validate_failures_returns_informative_messages(self): assert expected == messages def test_document_is_valid_when_using_or_later_licenses(self): - doc = Document(Version(2, 1), License.from_identifier('CC0-1.0')) + doc = Document(Version(2, 1), License.from_identifier('CC0-1.0'), + namespace='https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301') doc.creation_info.add_creator(Tool('ScanCode')) doc.creation_info.set_created_now() @@ -113,7 +115,8 @@ def test_document_is_valid_when_using_or_later_licenses(self): class TestWriters(TestCase): def _get_lgpl_doc(self, or_later=False): - doc = Document(Version(2, 1), License.from_identifier('CC0-1.0')) + doc = Document(Version(2, 1), License.from_identifier('CC0-1.0'), + namespace='https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301') doc.creation_info.add_creator(Tool('ScanCode')) doc.creation_info.set_created_now() diff --git a/tests/test_tag_value_parser.py b/tests/test_tag_value_parser.py index 9b1ec3e5f..e5ebc0cfb 100644 --- a/tests/test_tag_value_parser.py +++ b/tests/test_tag_value_parser.py @@ -35,6 +35,7 @@ def test_document(self): SPDXVersion: SPDX-2.1 # Comment. DataLicense: CC0-1.0 + DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 DocumentComment: This is a sample spreadsheet ''' self.l.input(data) @@ -42,8 +43,13 @@ def test_document(self): self.token_assert_helper(self.l.token(), 'LINE', 'SPDX-2.1', 2) self.token_assert_helper(self.l.token(), 'DOC_LICENSE', 'DataLicense', 4) self.token_assert_helper(self.l.token(), 'LINE', 'CC0-1.0', 4) - self.token_assert_helper(self.l.token(), 'DOC_COMMENT', 'DocumentComment', 5) - self.token_assert_helper(self.l.token(), 'TEXT', 'This is a sample spreadsheet', 5) + self.token_assert_helper(self.l.token(), 'DOC_NAMESPACE', + 'DocumentNamespace', 5) + self.token_assert_helper(self.l.token(), 'LINE', + 'https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301', + 5) + self.token_assert_helper(self.l.token(), 'DOC_COMMENT', 'DocumentComment', 6) + self.token_assert_helper(self.l.token(), 'TEXT', 'This is a sample spreadsheet', 6) def test_creation_info(self): data = ''' @@ -105,7 +111,8 @@ class TestParser(TestCase): document_str = '\n'.join([ 'SPDXVersion: SPDX-2.1', 'DataLicense: CC0-1.0', - 'DocumentComment: Sample Comment' + 'DocumentComment: Sample Comment', + 'DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301' ]) creation_str = '\n'.join([ @@ -170,6 +177,7 @@ def test_doc(self): assert document.version == Version(major=2, minor=1) assert document.data_license.identifier == 'CC0-1.0' assert document.comment == 'Sample Comment' + assert document.namespace == 'https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301' def test_creation_info(self): document, error = self.p.parse(self.complete_str) @@ -208,4 +216,4 @@ def test_file(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()