Source code for cyclonedx_py.utils.conda

# encoding: utf-8

# This file is part of CycloneDX Python Lib
#
# 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-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

import json
import sys
from json import JSONDecodeError
from typing import Optional, Tuple
from urllib.parse import urlparse

# See https://github.com/package-url/packageurl-python/issues/65
from packageurl import PackageURL  # type: ignore

if sys.version_info >= (3, 8):
    from typing import TypedDict
else:
    from typing_extensions import TypedDict


[docs] class CondaPackage(TypedDict): """ Internal package for unifying Conda package definitions to. """
[docs] base_url: str
[docs] build_number: Optional[int]
[docs] build_string: str
[docs] channel: str
[docs] dist_name: str
[docs] name: str
[docs] platform: str
[docs] version: str
[docs] package_format: Optional[str]
[docs] md5_hash: Optional[str]
[docs] def conda_package_to_purl(pkg: CondaPackage) -> PackageURL: """ Return the purl for the specified package. See https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#conda """ if pkg['channel'] == 'pypi' and pkg['platform'] == 'pypi': return _conda_package_to_pypi_purl(pkg) qualifiers = { 'build': pkg['build_string'], 'channel': pkg['channel'], 'subdir': pkg['platform'], } if pkg['package_format'] is not None: qualifiers['type'] = str(pkg['package_format']) purl = PackageURL( type='conda', name=pkg['name'], version=pkg['version'], qualifiers=qualifiers ) return purl
[docs] def _conda_package_to_pypi_purl(pkg: CondaPackage) -> PackageURL: """ Return the purl for a pip-installed package in a conda environment. These packages are listed as if from the pseudo-channel "pypi". See https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#pypi """ name = pkg['name'].lower().replace('_', '-') purl = PackageURL( type='pypi', name=name, version=pkg['version'] ) return purl
[docs] def parse_conda_json_to_conda_package(conda_json_str: str) -> Optional[CondaPackage]: try: package_data = json.loads(conda_json_str) except JSONDecodeError as e: raise ValueError(f'Invalid JSON supplied - cannot be parsed: {conda_json_str}') from e if not isinstance(package_data, dict): return None package_data.setdefault('package_format', None) package_data.setdefault('md5_hash', None) return CondaPackage(package_data) # type: ignore # @FIXME write proper type safe dict at this point
[docs] def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Optional[CondaPackage]: """ Helper method for parsing a line of output from `conda list --explicit` into our internal `CondaPackage` object. Params: conda_list_str: Line of output from `conda list --explicit` Returns: Instance of `CondaPackage` else `None`. """ line = conda_list_str.strip() if '' == line or line[0] in ['#', '@']: # Skip comments, @EXPLICT or empty lines return None # Remove any hash package_hash = None if '#' in line: *_line_parts, package_hash = line.split('#') line = ''.join(*_line_parts) package_parts = line.split('/') if len(package_parts) < 2: raise ValueError(f'Unexpected format in {package_parts}') *_package_url_parts, package_arch, package_name_version_build_string = package_parts package_url = urlparse('/'.join(_package_url_parts)) package_name, build_version, build_string, package_format = split_package_string(package_name_version_build_string) build_string, build_number = split_package_build_string(build_string) return CondaPackage( base_url=package_url.geturl(), build_number=build_number, build_string=build_string, channel=package_url.path[1:], dist_name=f'{package_name}-{build_version}-{build_string}', name=package_name, platform=package_arch, version=build_version, package_format=package_format, md5_hash=package_hash )
[docs] def split_package_string(package_name_version_build_string: str) -> Tuple[str, str, str, str]: """Helper method for parsing package_name_version_build_string. Returns: Tuple (package_name, build_version, build_string) """ package_nvbs_parts = package_name_version_build_string.split('-') if len(package_nvbs_parts) < 3: raise ValueError(f'Unexpected format in {package_nvbs_parts}') *_package_name_parts, build_version, build_string = package_nvbs_parts package_name = '-'.join(_package_name_parts) # Split package_format (.conda or .tar.gz) at the end _pos = build_string.find('.') package_format = build_string[_pos + 1:] build_string = build_string[0:_pos] return package_name, build_version, build_string, package_format
[docs] def split_package_build_string(build_string: str) -> Tuple[str, Optional[int]]: """Helper method for parsing build_string. Returns: Tuple (build_string, build_number) """ if '' == build_string: return '', None if build_string.isdigit(): return '', int(build_string) _pos = build_string.rindex('_') if '_' in build_string else -1 if _pos >= 1: # Build number will be the last part - check if it is an integer # Updated logic given https://github.com/CycloneDX/cyclonedx-python-lib/issues/65 build_number = build_string[_pos + 1:] if build_number.isdigit(): return build_string, int(build_number) return build_string, None