Skip to content

Commit f50472d

Browse files
committed
Add ExternalDocumentRef class
Signed-off-by: Yash Nisar <[email protected]>
1 parent e77eb3d commit f50472d

File tree

14 files changed

+382
-4
lines changed

14 files changed

+382
-4
lines changed

data/SPDXRdfExample.rdf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
</CreationInfo>
1616
</creationInfo>
1717
<specVersion>SPDX-2.1</specVersion>
18+
<externalDocumentRef>
19+
<ExternalDocumentRef>
20+
<externalDocumentId>DocumentRef-spdx-tool-2.1</externalDocumentId>
21+
<spdxDocument rdf:resource="https://spdx.org/spdxdocs/spdx-tools-v2.1-3F2504E0-4F89-41D3-9A0C-0305E82C3301"/>
22+
<checksum>
23+
<Checksum>
24+
<checksumValue>d6a770ba38583ed4bb4525bd96e50461655d2759</checksumValue>
25+
<algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
26+
</Checksum>
27+
</checksum>
28+
</ExternalDocumentRef>
29+
</externalDocumentRef>
1830
<referencesFile>
1931
<File rdf:nodeID="A0">
2032
<licenseConcluded>

data/SPDXSimpleTag.tag

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DocumentName: Sample_Document-V2.1
55
SPDXID: SPDXRef-DOCUMENT
66
DocumentNamespace: https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301
77
DocumentComment: <text>Sample Comment</text>
8+
ExternalDocumentRef:DocumentRef-spdx-tool-2.1 https://spdx.org/spdxdocs/spdx-tools-v2.1-3F2504E0-4F89-41D3-9A0C-0305E82C3301 SHA1: d6a770ba38583ed4bb4525bd96e50461655d2759
89

910
# Creation info
1011
Creator: Person: Bob ([email protected])

spdx/document.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from functools import total_ordering
1818

1919
from spdx import config
20+
from spdx.external_document_ref import ExternalDocumentRef
2021

2122

2223
def _add_parens(required, text):
@@ -192,6 +193,8 @@ class Document(object):
192193
- name: Name of the document. Mandatory, one. Type: str.
193194
- spdx_id: SPDX Identifier for the document to refer to itself in
194195
relationship to other elements. Mandatory, one. Type: str.
196+
- ext_document_references: External SPDX documents referenced within the
197+
given SPDX document. Optional, one or many. Type: ExternalDocumentRef
195198
- comment: Comments on the SPDX file, optional one. Type: str
196199
- namespace: SPDX document specific namespace. Mandatory, one. Type: str
197200
- creation_info: SPDX file creation info. Mandatory, one. Type: CreationInfo
@@ -210,6 +213,7 @@ def __init__(self, version=None, data_license=None, name=None, spdx_id=None,
210213
self.data_license = data_license
211214
self.name = name
212215
self.spdx_id = spdx_id
216+
self.ext_document_references = []
213217
self.comment = comment
214218
self.namespace = namespace
215219
self.creation_info = CreationInfo()
@@ -223,6 +227,9 @@ def add_review(self, review):
223227
def add_extr_lic(self, lic):
224228
self.extracted_licenses.append(lic)
225229

230+
def add_ext_document_reference(self, ext_doc_ref):
231+
self.ext_document_references.append(ext_doc_ref)
232+
226233
@property
227234
def files(self):
228235
return self.package.files
@@ -248,6 +255,7 @@ def validate(self, messages=None):
248255
and self.validate_name(messages)
249256
and self.validate_spdx_id(messages)
250257
and self.validate_namespace(messages)
258+
and self.validate_ext_document_references(messages)
251259
and self.validate_creation_info(messages)
252260
and self.validate_package(messages)
253261
and self.validate_extracted_licenses(messages)
@@ -312,6 +320,21 @@ def validate_spdx_id(self, messages=None):
312320
messages.append('Invalid Document SPDX Identifier value.')
313321
return False
314322

323+
def validate_ext_document_references(self, messages=None):
324+
# FIXME: messages should be returned
325+
messages = messages if messages is not None else []
326+
327+
valid = True
328+
for doc in self.ext_document_references:
329+
if isinstance(doc, ExternalDocumentRef):
330+
valid = doc.validate(messages) and valid
331+
else:
332+
messages.append(
333+
'External document references must be of the type '
334+
'spdx.document.ExternalDocumentRef and not ' + type(doc))
335+
valid = False
336+
return valid
337+
315338
def validate_reviews(self, messages=None):
316339
# FIXME: messages should be returned
317340
messages = messages if messages is not None else []

spdx/external_document_ref.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
2+
# Copyright (c) 2018 Yash M. Nisar
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from __future__ import absolute_import
14+
from __future__ import print_function
15+
from __future__ import unicode_literals
16+
17+
from functools import total_ordering
18+
19+
20+
@total_ordering
21+
class ExternalDocumentRef(object):
22+
"""
23+
External Document References entity that contains the following fields :
24+
- external_document_id: A unique string containing letters, numbers, '.',
25+
'-' or '+'.
26+
- spdx_document_uri: The unique ID of the SPDX document being referenced.
27+
- check_sum: The checksum of the referenced SPDX document.
28+
"""
29+
30+
def __init__(self, external_document_id=None, spdx_document_uri=None,
31+
check_sum=None):
32+
self.external_document_id = external_document_id
33+
self.spdx_document_uri = spdx_document_uri
34+
self.check_sum = check_sum
35+
36+
def __eq__(self, other):
37+
return (
38+
isinstance(other, ExternalDocumentRef)
39+
and self.external_document_id == other.external_document_id
40+
and self.spdx_document_uri == other.spdx_document_uri
41+
and self.check_sum == other.check_sum
42+
)
43+
44+
def __lt__(self, other):
45+
return (
46+
(self.external_document_id, self.spdx_document_uri,
47+
self.check_sum) <
48+
(other.external_document_id, other.spdx_document_uri,
49+
other.check_sum,)
50+
)
51+
52+
def validate(self, messages=None):
53+
"""
54+
Validate all fields of the ExternalDocumentRef class and update the
55+
messages list with user friendly error messages for display.
56+
"""
57+
messages = messages if messages is not None else []
58+
59+
return (self.validate_ext_doc_id(messages) and
60+
self.validate_spdx_doc_uri(messages) and
61+
self.validate_chksum(messages)
62+
)
63+
64+
def validate_ext_doc_id(self, messages=None):
65+
messages = messages if messages is not None else []
66+
67+
if self.external_document_id:
68+
return True
69+
else:
70+
messages.append('ExternalDocumentRef has no External Document ID.')
71+
return False
72+
73+
def validate_spdx_doc_uri(self, messages=None):
74+
messages = messages if messages is not None else []
75+
76+
if self.spdx_document_uri:
77+
return True
78+
else:
79+
messages.append('ExternalDocumentRef has no SPDX Document URI.')
80+
return False
81+
82+
def validate_chksum(self, messages=None):
83+
messages = messages if messages is not None else []
84+
85+
if self.check_sum:
86+
return True
87+
else:
88+
messages.append('ExternalDocumentRef has no Checksum.')
89+
return False

spdx/parsers/lexers/tagvalue.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Lexer(object):
2525
'SPDXID': 'DOC_SPDX_ID',
2626
'DocumentComment': 'DOC_COMMENT',
2727
'DocumentNamespace': 'DOC_NAMESPACE',
28+
'ExternalDocumentRef': 'EXT_DOC_REF',
2829
# Creation info
2930
'Creator': 'CREATOR',
3031
'Created': 'CREATED',
@@ -86,7 +87,8 @@ class Lexer(object):
8687

8788
tokens = ['TEXT', 'TOOL_VALUE', 'UNKNOWN_TAG',
8889
'ORG_VALUE', 'PERSON_VALUE',
89-
'DATE', 'LINE', 'CHKSUM'] + list(reserved.values())
90+
'DATE', 'LINE', 'CHKSUM', 'DOC_REF_ID',
91+
'DOC_URI', 'EXT_DOC_REF_CHKSUM'] + list(reserved.values())
9092

9193
def t_text(self, t):
9294
r':\s*<text>'
@@ -114,6 +116,21 @@ def t_CHKSUM(self, t):
114116
t.value = t.value[1:].strip()
115117
return t
116118

119+
def t_DOC_REF_ID(self, t):
120+
r':\s*DocumentRef-([A-Za-z0-9\+\.\-]+)'
121+
t.value = t.value[1:].strip()
122+
return t
123+
124+
def t_DOC_URI(self, t):
125+
r'\s*((ht|f)tps?:\/\/\S*)'
126+
t.value = t.value.strip()
127+
return t
128+
129+
def t_EXT_DOC_REF_CHKSUM(self, t):
130+
r'\s*SHA1:\s*[a-f0-9]{40,40}'
131+
t.value = t.value[1:].strip()
132+
return t
133+
117134
def t_TOOL_VALUE(self, t):
118135
r':\s*Tool:.+'
119136
t.value = t.value[1:].strip()

spdx/parsers/rdf.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
'LL_VALUE': 'Invalid licenseListVersion \'{0}\' must be of the format N.N where N is a number',
4141
'CREATED_VALUE': 'Invalid created value \'{0}\' must be date in ISO 8601 format.',
4242
'CREATOR_VALUE': 'Invalid creator value \'{0}\' must be Organization, Tool or Person.',
43+
'EXT_DOC_REF_VALUE': 'Failed to extract {0} from ExternalDocumentRef.',
4344
'PKG_SUPPL_VALUE': 'Invalid package supplier value \'{0}\' must be Organization, Person or NOASSERTION.',
4445
'PKG_ORIGINATOR_VALUE': 'Invalid package supplier value \'{0}\' must be Organization, Person or NOASSERTION.',
4546
'PKG_DOWN_LOC': 'Invalid package download location value \'{0}\' must be a url or NONE or NOASSERTION',
@@ -746,6 +747,9 @@ def parse(self, fil):
746747
for s, _p, o in self.graph.triples((None, RDF.type, self.spdx_namespace['SpdxDocument'])):
747748
self.parse_doc_fields(s)
748749

750+
for s, _p, o in self.graph.triples((None, RDF.type, self.spdx_namespace['ExternalDocumentRef'])):
751+
self.parse_ext_doc_ref(s)
752+
749753
for s, _p, o in self.graph.triples((None, RDF.type, self.spdx_namespace['CreationInfo'])):
750754
self.parse_creation_info(s)
751755

@@ -846,3 +850,37 @@ def parse_doc_fields(self, doc_term):
846850
except CardinalityError:
847851
self.more_than_one_error('Document comment')
848852
break
853+
854+
def parse_ext_doc_ref(self, ext_doc_ref_term):
855+
"""
856+
Parses the External Document ID, SPDX Document URI and Checksum.
857+
"""
858+
for _s, _p, o in self.graph.triples(
859+
(ext_doc_ref_term,
860+
self.spdx_namespace['externalDocumentId'],
861+
None)):
862+
try:
863+
self.builder.set_ext_doc_id(self.doc, six.text_type(o))
864+
except SPDXValueError:
865+
self.value_error('EXT_DOC_REF_VALUE', 'External Document ID')
866+
break
867+
868+
for _s, _p, o in self.graph.triples(
869+
(ext_doc_ref_term,
870+
self.spdx_namespace['spdxDocument'],
871+
None)):
872+
try:
873+
self.builder.set_spdx_doc_uri(self.doc, six.text_type(o))
874+
except SPDXValueError:
875+
self.value_error('EXT_DOC_REF_VALUE', 'SPDX Document URI')
876+
break
877+
878+
for _s, _p, checksum in self.graph.triples(
879+
(ext_doc_ref_term, self.spdx_namespace['checksum'], None)):
880+
for _, _, value in self.graph.triples(
881+
(checksum, self.spdx_namespace['checksumValue'], None)):
882+
try:
883+
self.builder.set_chksum(self.doc, six.text_type(value))
884+
except SPDXValueError:
885+
self.value_error('EXT_DOC_REF_VALUE', 'Checksum')
886+
break

spdx/parsers/rdfbuilders.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,20 @@ def reset_document(self):
135135
self.doc_spdx_id_set = False
136136

137137

138+
class ExternalDocumentRefBuilder(tagvaluebuilders.ExternalDocumentRefBuilder):
139+
140+
def set_chksum(self, doc, chk_sum):
141+
"""
142+
Sets the external document reference's check sum, if not already set.
143+
chk_sum - The checksum value in the form of a string.
144+
"""
145+
if chk_sum:
146+
doc.ext_document_references[-1].check_sum = checksum.Algorithm(
147+
'SHA1', chk_sum)
148+
else:
149+
raise SPDXValueError('ExternalDocumentRef::Checksum')
150+
151+
138152
class EntityBuilder(tagvaluebuilders.EntityBuilder):
139153

140154
def __init__(self):
@@ -370,7 +384,8 @@ def add_review_comment(self, doc, comment):
370384
raise OrderError('ReviewComment')
371385

372386

373-
class Builder(DocBuilder, EntityBuilder, CreationInfoBuilder, PackageBuilder, FileBuilder, ReviewBuilder):
387+
class Builder(DocBuilder, EntityBuilder, CreationInfoBuilder, PackageBuilder,
388+
FileBuilder, ReviewBuilder, ExternalDocumentRefBuilder):
374389

375390
def __init__(self):
376391
super(Builder, self).__init__()

spdx/parsers/tagvalue.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
'DOC_VERSION_VALUE_TYPE': 'Invalid SPDXVersion value, must be SPDX-M.N where M and N are numbers. Line: {0}',
4242
'DOC_NAME_VALUE': 'DocumentName must be single line of text, line: {0}',
4343
'DOC_SPDX_ID_VALUE': 'Invalid SPDXID value, SPDXID must be SPDXRef-DOCUMENT, line: {0}',
44+
'EXT_DOC_REF_VALUE': 'ExternalDocumentRef must contain External Document ID, SPDX Document URI and Checksum'
45+
'in the standard format, line:{0}.',
4446
'DOC_COMMENT_VALUE_TYPE': 'DocumentComment value must be free form text between <text></text> tags, line:{0}',
4547
'DOC_NAMESPACE_VALUE': 'Invalid DocumentNamespace value {0}, must contain a scheme (e.g. "https:") '
4648
'and should not contain the "#" delimiter, line:{1}',
@@ -114,6 +116,7 @@ def p_attrib(self, p):
114116
| data_lics
115117
| doc_name
116118
| doc_spdx_id
119+
| ext_doc_ref
117120
| doc_comment
118121
| doc_namespace
119122
| creator
@@ -1164,6 +1167,30 @@ def p_doc_spdx_id_2(self, p):
11641167
"""doc_spdx_id : DOC_SPDX_ID error"""
11651168
self.error = True
11661169
msg = ERROR_MESSAGES['DOC_SPDX_ID_VALUE'].format(p.lineno(1))
1170+
1171+
def p_ext_doc_refs_1(self, p):
1172+
"""ext_doc_ref : EXT_DOC_REF DOC_REF_ID DOC_URI EXT_DOC_REF_CHKSUM"""
1173+
try:
1174+
if six.PY2:
1175+
doc_ref_id = p[2].decode(encoding='utf-8')
1176+
doc_uri = p[3].decode(encoding='utf-8')
1177+
ext_doc_chksum = p[4].decode(encoding='utf-8')
1178+
else:
1179+
doc_ref_id = p[2]
1180+
doc_uri = p[3]
1181+
ext_doc_chksum = p[4]
1182+
1183+
self.builder.add_ext_doc_refs(self.document, doc_ref_id, doc_uri,
1184+
ext_doc_chksum)
1185+
except SPDXValueError:
1186+
self.error = True
1187+
msg = ERROR_MESSAGES['EXT_DOC_REF_VALUE'].format(p.lineno(2))
1188+
self.logger.log(msg)
1189+
1190+
def p_ext_doc_refs_2(self, p):
1191+
"""ext_doc_ref : EXT_DOC_REF error"""
1192+
self.error = True
1193+
msg = ERROR_MESSAGES['EXT_DOC_REF_VALUE'].format(p.lineno(1))
11671194
self.logger.log(msg)
11681195

11691196
def p_spdx_version_1(self, p):

0 commit comments

Comments
 (0)