PyPACoreMetadataParser.java
- /*
- * This file is part of dependency-check-core.
- *
- * 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.
- *
- * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
- */
- package org.owasp.dependencycheck.utils;
- import org.apache.commons.lang3.StringUtils;
- import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import java.io.BufferedReader;
- import java.io.File;
- import java.io.IOException;
- import java.math.BigDecimal;
- import java.nio.charset.StandardCharsets;
- import java.nio.file.Files;
- import java.util.Properties;
- /**
- * A utility class to handle Python Packaging Authority (PyPA) core metadata files. It was created based on the
- * <a href="https://packaging.python.org/en/latest/specifications/core-metadata/">specification by PyPA</a> for
- * version 2.2
- *
- * @author Hans Aikema
- */
- public final class PyPACoreMetadataParser {
- /**
- * The logger.
- */
- private static final Logger LOGGER = LoggerFactory.getLogger(PyPACoreMetadataParser.class);
- /**
- * The largest major version considered by this parser
- */
- private static final int SUPPORTED_MAJOR_UPPERBOUND = 2;
- /**
- * The largest version of the specification considered during coding of this parser
- */
- private static final BigDecimal MAX_SUPPORTED_VERSION = BigDecimal.valueOf(22, 1);
- private PyPACoreMetadataParser() {
- // hide constructor for utility class
- }
- /**
- * Loads all key/value pairs from PyPA metadata specifications¶.
- *
- * @param file
- * The Wheel metadata of a Python package as a File
- *
- * @return The metadata properties read from the file
- * @throws AnalysisException thrown if there is an analysis exception
- */
- public static Properties getProperties(File file) throws AnalysisException {
- try (BufferedReader utf8Reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
- return getProperties(utf8Reader);
- } catch (IOException | IllegalArgumentException e) {
- throw new AnalysisException("Error parsing PyPA core-metadata file", e);
- }
- }
- /**
- * Loads all key/value pairs from PyPA metadata specifications¶.
- *
- * @param utf8Reader
- * The Wheel metadata of a Python package as a BufferedReader
- *
- * @return The metadata properties read from the utf8Reader
- * @throws java.io.IOException thrown if there is error reading the properties
- */
- public static Properties getProperties(final BufferedReader utf8Reader) throws IOException {
- final Properties result = new Properties();
- String line = utf8Reader.readLine();
- StringBuilder singleHeader = null;
- boolean inDescription = false;
- while (line != null && !line.isEmpty()) {
- if (inDescription && line.startsWith(" |")) {
- singleHeader.append('\n').append(line.substring(8));
- } else if (singleHeader != null && line.startsWith(" ")) {
- singleHeader.append(line.substring(1));
- } else {
- if (singleHeader != null) {
- parseAndAddHeader(result, singleHeader);
- }
- singleHeader = new StringBuilder(line);
- inDescription = line.startsWith("Description:");
- }
- line = utf8Reader.readLine();
- }
- if (singleHeader != null) {
- parseAndAddHeader(result, singleHeader);
- }
- // ignore a body if any (description is allowed to be the message body)
- return result;
- }
- /**
- * Add a single metadata keyvalue pair to the metadata. When the given metadataHeader cannot be parsed as a '{@code key: value}'
- * line a warning is emitted and the line is ignored.
- *
- * @param metadata
- * The collected metadata to which the new metadataHeader must be added
- * @param metadataHeader
- * A single uncollapsed header line of the metadata
- *
- * @throws IllegalArgumentException
- * When the given metadataHeader has a key {@code Metadata-Version} and the value holds a major version that is larger
- * than the highest supported metadata version. As defined by the specification: <blockquote>Automated tools consuming
- * metadata SHOULD warn if metadata_version is greater than the highest version they support, and MUST fail if
- * metadata_version has a greater major version than the highest version they support (as described in PEP 440, the
- * major version is the value before the first dot).</blockquote>
- */
- private static void parseAndAddHeader(final Properties metadata, final StringBuilder metadataHeader) {
- final String[] keyValue = StringUtils.split(metadataHeader.toString(), ":", 2);
- if (keyValue.length != 2) {
- LOGGER.warn("Invalid mailheader format encountered in Wheel Metadata, not a \"key: value\" string");
- return;
- }
- final String key = keyValue[0];
- final String value = keyValue[1].trim();
- if ("Metadata-Version".equals(key)) {
- final int majorVersion = Integer.parseInt(value.substring(0, value.indexOf('.')), 10);
- final BigDecimal version = new BigDecimal(value);
- if (majorVersion > SUPPORTED_MAJOR_UPPERBOUND) {
- throw new IllegalArgumentException(String.format(
- "Unsupported PyPA Wheel metadata. Metadata-Version " + "is '%s', largest supported major is %d", value,
- SUPPORTED_MAJOR_UPPERBOUND));
- } else if (version.compareTo(MAX_SUPPORTED_VERSION) > 0 && LOGGER.isWarnEnabled()) {
- LOGGER.warn(String.format("Wheel metadata Metadata-Version (%s) has a larger minor version than the highest known "
- + "supported Metadata specification (%s) continuing with best effort", value,
- MAX_SUPPORTED_VERSION));
- }
- }
- metadata.setProperty(key, value);
- }
- }