blob: c039051ee0fb890a514d3d2b7eea32137f62ae00 [file] [log] [blame] [edit]
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os.path
from typing import List, Callable
import re
import constants
from metadata import Metadata
from pathlib import Path
from mapper import MapperException
from license_type import LicenseType
# The mandatory metadata fields for a single dependency.
KNOWN_FIELDS = {
"Name", # Short name (for header on about:credits).
"URL", # Project home page.
"License", # Software license.
"License File", # Relative paths to license texts.
"Shipped", # Whether the package is in the shipped product.
"Version", # The version for the package.
"Revision", # This is equivalent to Version but Chromium is lenient.
}
# The metadata fields that can have multiple values.
MULTIVALUE_FIELDS = {
"License",
"License File",
}
# Line used to separate dependencies within the same metadata file.
PATTERN_DEPENDENCY_DIVIDER = re.compile(r"^-{20} DEPENDENCY DIVIDER -{20}$")
# The delimiter used to separate multiple values for one metadata field.
VALUE_DELIMITER = ","
def get_license_type(license: str) -> LicenseType:
"""Return the equivalent license type for the provided string license."""
if license in constants.RAW_LICENSE_TO_FORMATTED_DETAILS:
return constants.RAW_LICENSE_TO_FORMATTED_DETAILS[license][1]
raise None
def get_license_bp_name(license: str) -> str:
return constants.RAW_LICENSE_TO_FORMATTED_DETAILS[license][2]
def is_ignored_readme_chromium(path: str) -> bool:
return path in constants.IGNORED_README
def get_most_restrictive_type(licenses: List[str]) -> LicenseType:
"""Returns the most restrictive license according to the values of LicenseType."""
most_restrictive = LicenseType.UNKNOWN
for license in licenses:
if constants.RAW_LICENSE_TO_FORMATTED_DETAILS[license][
1].value > most_restrictive.value:
most_restrictive = constants.RAW_LICENSE_TO_FORMATTED_DETAILS[license][1]
return most_restrictive
def get_license_file_format(license: str):
"""Return a different representation of the license that is better suited
for file names."""
if license in constants.RAW_LICENSE_TO_FORMATTED_DETAILS:
return constants.RAW_LICENSE_TO_FORMATTED_DETAILS[license][0]
raise None
class InvalidMetadata(Exception):
"""This exception is raised when metadata is invalid."""
pass
def parse_chromium_readme_file(readme_path: str,
post_process_operation: Callable = None) -> Metadata:
"""Parses the metadata from the file.
Args:
readme_path: the path to a file from which to parse metadata.
post_process_operation: Operation done on the dictionary after parsing
metadata, this callable must return a dictionary.
Returns: the metadata for all dependencies described in the file.
Raises:
InvalidMetadata - Raised when the metadata can't be parsed correctly. This
could happen due to plenty of reasons (eg: unidentifiable license, license
file path does not exist or duplicate fields).
"""
field_lookup = {name.lower(): name for name in KNOWN_FIELDS}
dependencies = []
metadata = {}
for line in Path(readme_path).read_text().split("\n"):
line = line.strip()
# Skip empty lines.
if not line:
continue
# Check if a new dependency will be described.
if re.match(PATTERN_DEPENDENCY_DIVIDER, line):
# Save the metadata for the previous dependency.
if metadata:
dependencies.append(metadata)
metadata = {}
continue
# Otherwise, try to parse the field name and field value.
parts = line.split(": ", 1)
if len(parts) == 2:
raw_field, value = parts
field = field_lookup.get(raw_field.lower())
if field:
if field in metadata:
# Duplicate field for this dependency.
raise InvalidMetadata(f"duplicate '{field}' in {readme_path}")
if field in MULTIVALUE_FIELDS:
metadata[field] = [
entry.strip() for entry in value.split(VALUE_DELIMITER)
]
else:
metadata[field] = value
# The end of the file has been reached. Save the metadata for the
# last dependency, if available.
if metadata:
dependencies.append(metadata)
if len(dependencies) == 0:
raise Exception(
f"Failed to parse any valid metadata from \"{readme_path}\"")
try:
if post_process_operation is None:
post_process_operation = constants.POST_PROCESS_OPERATION.get(readme_path,
lambda
_metadata: _metadata)
metadata = Metadata(post_process_operation(dependencies[0]))
except MapperException:
raise Exception(f"Failed to post-process {readme_path}")
for license in metadata.get_licenses():
if not license in constants.RAW_LICENSE_TO_FORMATTED_DETAILS:
raise InvalidMetadata(
f"\"{readme_path}\" contains unidentified license \"{license}\"")
return metadata
def resolve_license_path(readme_chromium_path: str,
license_path: str) -> str:
"""
Resolves the relative path from the repository root to the license file.
:param readme_chromium_path: Relative path to the README.chromium starting
from the root of the repository.
:param license_path: The field value of `License File` in the README.chromium.
If the value of the license_path starts with `//` then that means that the
license file path is already relative from the repo path. Otherwise, it is
assumed that the provided path is relative from the README.chromium path.
:return: The relative path from the repository root to the declared license
file.
"""
if license_path.startswith("//"):
# This is an relative path that starts from the root of external/cronet
# repository, we should not use the directory path for resolution here.
# See https://source.chromium.org/chromium/chromium/src/+/main:third_party/rust/bytes/v1/README.chromium as
# an example of such case.
return license_path[2:]
# Relative path from the README.chromium, append the path from root of repo
# until the README.chromium so it becomes a relative path from the root of
# repo.
return os.path.join(readme_chromium_path, license_path)