PyPACoreMetadataParser.java

  1. /*
  2.  * This file is part of dependency-check-core.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *     http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  *
  16.  * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
  17.  */
  18. package org.owasp.dependencycheck.utils;

  19. import org.apache.commons.lang3.StringUtils;
  20. import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
  21. import org.slf4j.Logger;
  22. import org.slf4j.LoggerFactory;

  23. import java.io.BufferedReader;
  24. import java.io.File;
  25. import java.io.IOException;
  26. import java.math.BigDecimal;
  27. import java.nio.charset.StandardCharsets;
  28. import java.nio.file.Files;
  29. import java.util.Properties;

  30. /**
  31.  * A utility class to handle Python Packaging Authority (PyPA) core metadata files. It was created based on the
  32.  * <a href="https://packaging.python.org/en/latest/specifications/core-metadata/">specification by PyPA</a> for
  33.  * version 2.2
  34.  *
  35.  * @author Hans Aikema
  36.  */
  37. public final class PyPACoreMetadataParser {

  38.     /**
  39.      * The logger.
  40.      */
  41.     private static final Logger LOGGER = LoggerFactory.getLogger(PyPACoreMetadataParser.class);

  42.     /**
  43.      * The largest major version considered by this parser
  44.      */
  45.     private static final int SUPPORTED_MAJOR_UPPERBOUND = 2;

  46.     /**
  47.      * The largest version of the specification considered during coding of this parser
  48.      */
  49.     private static final BigDecimal MAX_SUPPORTED_VERSION = BigDecimal.valueOf(22, 1);

  50.     private PyPACoreMetadataParser() {
  51.         // hide constructor for utility class
  52.     }
  53.     /**
  54.      * Loads all key/value pairs from PyPA metadata specifications¶.
  55.      *
  56.      * @param file
  57.      *         The Wheel metadata of a Python package as a File
  58.      *
  59.      * @return The metadata properties read from the file
  60.      * @throws AnalysisException thrown if there is an analysis exception
  61.      */
  62.     public static Properties getProperties(File file) throws AnalysisException {
  63.         try (BufferedReader utf8Reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
  64.             return getProperties(utf8Reader);
  65.         } catch (IOException | IllegalArgumentException e) {
  66.             throw new AnalysisException("Error parsing PyPA core-metadata file", e);
  67.         }
  68.     }

  69.     /**
  70.      * Loads all key/value pairs from PyPA metadata specifications¶.
  71.      *
  72.      * @param utf8Reader
  73.      *         The Wheel metadata of a Python package as a BufferedReader
  74.      *
  75.      * @return The metadata properties read from the utf8Reader
  76.      * @throws java.io.IOException thrown if there is error reading the properties
  77.      */
  78.     public static Properties getProperties(final BufferedReader utf8Reader) throws IOException {
  79.         final Properties result = new Properties();
  80.         String line = utf8Reader.readLine();
  81.         StringBuilder singleHeader = null;
  82.         boolean inDescription = false;
  83.         while (line != null && !line.isEmpty()) {
  84.             if (inDescription && line.startsWith("       |")) {
  85.                 singleHeader.append('\n').append(line.substring(8));
  86.             } else if (singleHeader != null && line.startsWith(" ")) {
  87.                 singleHeader.append(line.substring(1));
  88.             } else {
  89.                 if (singleHeader != null) {
  90.                     parseAndAddHeader(result, singleHeader);
  91.                 }
  92.                 singleHeader = new StringBuilder(line);
  93.                 inDescription = line.startsWith("Description:");
  94.             }
  95.             line = utf8Reader.readLine();
  96.         }
  97.         if (singleHeader != null) {
  98.             parseAndAddHeader(result, singleHeader);
  99.         }
  100.         // ignore a body if any (description is allowed to be the message body)
  101.         return result;
  102.     }

  103.     /**
  104.      * Add a single metadata keyvalue pair to the metadata. When the given metadataHeader cannot be parsed as a '{@code key: value}'
  105.      * line a warning is emitted and the line is ignored.
  106.      *
  107.      * @param metadata
  108.      *         The collected metadata to which the new metadataHeader must be added
  109.      * @param metadataHeader
  110.      *         A single uncollapsed header line of the metadata
  111.      *
  112.      * @throws IllegalArgumentException
  113.      *         When the given metadataHeader has a key {@code Metadata-Version} and the value holds a major version that is larger
  114.      *         than the highest supported metadata version. As defined by the specification: <blockquote>Automated tools consuming
  115.      *         metadata SHOULD warn if metadata_version is greater than the highest version they support, and MUST fail if
  116.      *         metadata_version has a greater major version than the highest version they support (as described in PEP 440, the
  117.      *         major version is the value before the first dot).</blockquote>
  118.      */
  119.     private static void parseAndAddHeader(final Properties metadata, final StringBuilder metadataHeader) {
  120.         final String[] keyValue = StringUtils.split(metadataHeader.toString(), ":", 2);
  121.         if (keyValue.length != 2) {
  122.             LOGGER.warn("Invalid mailheader format encountered in Wheel Metadata, not a \"key: value\" string");
  123.             return;
  124.         }
  125.         final String key = keyValue[0];
  126.         final String value = keyValue[1].trim();
  127.         if ("Metadata-Version".equals(key)) {
  128.             final int majorVersion = Integer.parseInt(value.substring(0, value.indexOf('.')), 10);
  129.             final BigDecimal version = new BigDecimal(value);
  130.             if (majorVersion > SUPPORTED_MAJOR_UPPERBOUND) {
  131.                 throw new IllegalArgumentException(String.format(
  132.                         "Unsupported PyPA Wheel metadata. Metadata-Version " + "is '%s', largest supported major is %d", value,
  133.                         SUPPORTED_MAJOR_UPPERBOUND));
  134.             } else if (version.compareTo(MAX_SUPPORTED_VERSION) > 0 && LOGGER.isWarnEnabled()) {
  135.                 LOGGER.warn(String.format("Wheel metadata Metadata-Version (%s) has a larger minor version than the highest known "
  136.                                           + "supported Metadata specification (%s) continuing with best effort", value,
  137.                                           MAX_SUPPORTED_VERSION));
  138.             }
  139.         }
  140.         metadata.setProperty(key, value);
  141.     }
  142. }