1# Copyright (c) 2014 Ahmed H. Ismail
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#     http://www.apache.org/licenses/LICENSE-2.0
6# Unless required by applicable law or agreed to in writing, software
7# distributed under the License is distributed on an "AS IS" BASIS,
8# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9# See the License for the specific language governing permissions and
10# limitations under the License.
11
12from __future__ import absolute_import
13from __future__ import print_function
14from __future__ import unicode_literals
15
16import hashlib
17
18from six.moves import reduce
19
20from spdx import checksum
21from spdx import creationinfo
22from spdx import document
23from spdx import utils
24
25
26class Package(object):
27
28    """
29    Represent an analyzed Package.
30    Fields:
31     - name : Mandatory, string.
32     - spdx_id: Uniquely identify any element in an SPDX document which may be
33     referenced by other elements. Mandatory, one. Type: str.
34     - version: Optional, string.
35     - file_name: Optional, string.
36     - supplier: Optional, Organization or Person or NO_ASSERTION.
37     - originator: Optional, Organization or Person.
38     - download_location: Mandatory, URL as string.
39     - files_analyzed: Indicates whether the file content of this package has
40     been available for or subjected to analysis when creating the SPDX
41     document. If "false" indicates packages that represent metadata or URI
42     references to a project, product, artifact, distribution or a component.
43     If set to "false", the package must not contain any files.
44     Optional, boolean.
45     - homepage: Optional, URL as string or NONE or NO_ASSERTION.
46     - verif_code: Mandatory string.
47     - check_sum: Optional , spdx.checksum.Algorithm.
48     - source_info: Optional string.
49     - conc_lics: Mandatory spdx.document.License or spdx.utils.SPDXNone or
50     - spdx.utils.NoAssert.
51     - license_declared : Mandatory spdx.document.License or spdx.utils.SPDXNone or
52     - spdx.utils.NoAssert.
53     - license_comment  : optional string.
54     - licenses_from_files: list of spdx.document.License or spdx.utils.SPDXNone or
55     - spdx.utils.NoAssert.
56     - cr_text: Copyright text, string , utils.NoAssert or utils.SPDXNone. Mandatory.
57     - summary: Optional str.
58     - description: Optional str.
59     - comment: Comments about the package being described, optional one.
60     Type: str
61     - files: List of files in package, atleast one.
62     - verif_exc_files : list of file names excluded from verification code or None.
63     - ext_pkg_refs : External references referenced within the given package.
64     Optional, one or many. Type: ExternalPackageRef
65    """
66
67    def __init__(self, name=None, spdx_id=None, download_location=None,
68                 version=None, file_name=None, supplier=None, originator=None):
69        self.name = name
70        self.spdx_id = spdx_id
71        self.version = version
72        self.file_name = file_name
73        self.supplier = supplier
74        self.originator = originator
75        self.download_location = download_location
76        self.files_analyzed = None
77        self.homepage = None
78        self.verif_code = None
79        self.check_sum = None
80        self.source_info = None
81        self.conc_lics = None
82        self.license_declared = None
83        self.license_comment = None
84        self.licenses_from_files = []
85        self.cr_text = None
86        self.summary = None
87        self.description = None
88        self.comment = None
89        self.files = []
90        self.verif_exc_files = []
91        self.pkg_ext_refs = []
92
93    def add_file(self, fil):
94        self.files.append(fil)
95
96    def add_lics_from_file(self, lics):
97        self.licenses_from_files.append(lics)
98
99    def add_exc_file(self, filename):
100        self.verif_exc_files.append(filename)
101
102    def add_pkg_ext_refs(self, pkg_ext_ref):
103        self.pkg_ext_refs.append(pkg_ext_ref)
104
105    def validate(self, messages=None):
106        """
107        Validate the package fields.
108        Append user friendly error messages to the `messages` list.
109        """
110        messages = self.validate_checksum(messages)
111        messages = self.validate_optional_str_fields(messages)
112        messages = self.validate_mandatory_str_fields(messages)
113        messages = self.validate_files(messages)
114        messages = self.validate_pkg_ext_refs(messages)
115        messages = self.validate_mandatory_fields(messages)
116        messages = self.validate_optional_fields(messages)
117
118        return messages
119
120    def validate_optional_fields(self, messages):
121        if self.originator and not isinstance(self.originator, (utils.NoAssert, creationinfo.Creator)):
122            messages = messages + [
123                'Package originator must be instance of '
124                'spdx.utils.NoAssert or spdx.creationinfo.Creator'
125            ]
126
127        if self.supplier and not isinstance(self.supplier, (utils.NoAssert, creationinfo.Creator)):
128            messages = messages + [
129                'Package supplier must be instance of '
130                'spdx.utils.NoAssert or spdx.creationinfo.Creator'
131            ]
132
133        return messages
134
135    def validate_pkg_ext_refs(self, messages=None):
136        for ref in self.pkg_ext_refs:
137            if isinstance(ref, ExternalPackageRef):
138                messages = ref.validate(messages)
139            else:
140                messages = messages + [
141                    'External package references must be of the type '
142                    'spdx.package.ExternalPackageRef and not ' + str(type(ref))
143                ]
144
145        return messages
146
147    def validate_mandatory_fields(self, messages):
148        if not isinstance(self.conc_lics, (utils.SPDXNone, utils.NoAssert, document.License)):
149            messages = messages + [
150                'Package concluded license must be instance of '
151                'spdx.utils.SPDXNone or spdx.utils.NoAssert or '
152                'spdx.document.License'
153            ]
154
155        if not isinstance(self.license_declared, (utils.SPDXNone, utils.NoAssert, document.License)):
156            messages = messages + [
157                'Package declared license must be instance of '
158                'spdx.utils.SPDXNone or spdx.utils.NoAssert or '
159                'spdx.document.License'
160            ]
161
162        # FIXME: this is obscure and unreadable
163        license_from_file_check = lambda prev, el: prev and isinstance(el, (document.License, utils.SPDXNone, utils.NoAssert))
164        if not reduce(license_from_file_check, self.licenses_from_files, True):
165            messages = messages + [
166                'Each element in licenses_from_files must be instance of '
167                'spdx.utils.SPDXNone or spdx.utils.NoAssert or '
168                'spdx.document.License'
169            ]
170
171        if not self.licenses_from_files:
172            messages = messages + [
173                'Package licenses_from_files can not be empty'
174            ]
175
176        return messages
177
178    def validate_files(self, messages):
179        if not self.files:
180            messages = messages + [
181                'Package must have at least one file.'
182            ]
183        else:
184            for f in self.files:
185                messages = f.validate(messages)
186
187        return messages
188
189    def validate_optional_str_fields(self, messages):
190        """Fields marked as optional and of type string in class
191        docstring must be of a type that provides __str__ method.
192        """
193        FIELDS = [
194            'file_name',
195            'version',
196            'homepage',
197            'source_info',
198            'summary',
199            'description',
200            'comment'
201        ]
202        messages = self.validate_str_fields(FIELDS, True, messages)
203
204        return messages
205
206    def validate_mandatory_str_fields(self, messages):
207        """Fields marked as Mandatory and of type string in class
208        docstring must be of a type that provides __str__ method.
209        """
210        FIELDS = ['name', 'spdx_id', 'download_location', 'verif_code', 'cr_text']
211        messages = self.validate_str_fields(FIELDS, False, messages)
212
213        return messages
214
215    def validate_str_fields(self, fields, optional, messages):
216        """Helper for validate_mandatory_str_field and
217        validate_optional_str_fields"""
218        for field_str in fields:
219            field = getattr(self, field_str)
220            if field is not None:
221                # FIXME: this does not make sense???
222                attr = getattr(field, '__str__', None)
223                if not callable(attr):
224                    messages = messages + [
225                        '{0} must provide __str__ method.'.format(field)
226                    ]
227                    # Continue checking.
228            elif not optional:
229                messages = messages + [
230                    'Package {0} can not be None.'.format(field_str)
231                ]
232
233        return messages
234
235    def validate_checksum(self, messages):
236        if not isinstance(self.check_sum, checksum.Algorithm):
237            messages = messages + [
238                'Package checksum must be instance of spdx.checksum.Algorithm'
239            ]
240        else:
241            if self.check_sum.identifier != 'SHA1':
242                messages = messages + ['File checksum algorithm must be SHA1']
243
244        return messages
245
246    def calc_verif_code(self):
247        hashes = []
248
249        for file_entry in self.files:
250            if (isinstance(file_entry.chk_sum, checksum.Algorithm) and
251                file_entry.chk_sum.identifier == 'SHA1'):
252                sha1 = file_entry.chk_sum.value
253            else:
254                sha1 = file_entry.calc_chksum()
255            hashes.append(sha1)
256
257        hashes.sort()
258
259        sha1 = hashlib.sha1()
260        sha1.update(''.join(hashes).encode('utf-8'))
261        return sha1.hexdigest()
262
263    def has_optional_field(self, field):
264        return getattr(self, field, None) is not None
265
266
267class ExternalPackageRef(object):
268    """
269    An External Reference allows a Package to reference an external source of
270    additional information, metadata, enumerations, asset identifiers, or
271    downloadable content believed to be relevant to the Package.
272    Fields:
273    - category: "SECURITY" or "PACKAGE-MANAGER" or "OTHER".
274    - pkg_ext_ref_type: A unique string containing letters, numbers, ".","-".
275    - locator: A unique string with no spaces necessary to access the
276    package-specific information, metadata, or content within the target
277    location.
278    - comment: To provide information about the purpose and target of the
279    reference.
280    """
281
282    def __init__(self, category=None, pkg_ext_ref_type=None, locator=None,
283                 comment=None):
284        self.category = category
285        self.pkg_ext_ref_type = pkg_ext_ref_type
286        self.locator = locator
287        self.comment = comment
288
289    def validate(self, messages=None):
290        """
291        Validate all fields of the ExternalPackageRef class and update the
292        messages list with user friendly error messages for display.
293        """
294        messages = self.validate_category(messages)
295        messages = self.validate_pkg_ext_ref_type(messages)
296        messages = self.validate_locator(messages)
297
298        return messages
299
300    def validate_category(self, messages=None):
301        if self.category is None:
302            messages = messages + ['ExternalPackageRef has no category.']
303
304        return messages
305
306    def validate_pkg_ext_ref_type(self, messages=None):
307        if self.pkg_ext_ref_type is None:
308            messages = messages + ['ExternalPackageRef has no type.']
309
310        return messages
311
312    def validate_locator(self, messages=None):
313        if self.locator is None:
314            messages = messages + ['ExternalPackageRef has no locator.']
315
316        return messages
317